Skip to content

Commit bf4e082

Browse files
committed
feat: support key algorithms and add support for der/pem in PublicKey
1 parent 78a1330 commit bf4e082

5 files changed

Lines changed: 141 additions & 105 deletions

File tree

biscuit_auth.pyi

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ PublicKeyProvider: TypeAlias = Union[
3333
Callable[[], PublicKey], Callable[[int], PublicKey]
3434
]
3535

36+
class Algorithm:
37+
pass
38+
3639
class BiscuitBuilder:
3740
# Create a builder from a datalog snippet and optional parameter values
3841
#
@@ -406,12 +409,6 @@ class PublicKey:
406409
# :rtype: list
407410
def to_bytes(self) -> bytes: ...
408411

409-
# Serializes a public key to a hexadecimal string
410-
#
411-
# :return: the public key bytes (hex-encoded)
412-
# :rtype: str
413-
def to_hex(self) -> str: ...
414-
415412
# Deserializes a public key from raw bytes
416413
#
417414
# :param data: the raw bytes
@@ -428,7 +425,7 @@ class PublicKey:
428425
# :return: the public key
429426
# :rtype: PublicKey
430427
@classmethod
431-
def from_hex(cls, data: str) -> PublicKey: ...
428+
def __new__(cls, data: str) -> PublicKey: ...
432429

433430
# ed25519 private key
434431
class PrivateKey:
@@ -438,12 +435,6 @@ class PrivateKey:
438435
# :rtype: list
439436
def to_bytes(self) -> bytes: ...
440437

441-
# Serializes a private key to a hexadecimal string
442-
#
443-
# :return: the private key bytes (hex-encoded)
444-
# :rtype: str
445-
def to_hex(self) -> str: ...
446-
447438
# Deserializes a private key from raw bytes
448439
#
449440
# :param data: the raw bytes
@@ -460,7 +451,7 @@ class PrivateKey:
460451
# :return: the private key
461452
# :rtype: PrivateKey
462453
@classmethod
463-
def from_hex(cls, data: str) -> PrivateKey: ...
454+
def __new__(cls, data: str) -> PrivateKey: ...
464455

465456
# A single datalog Fact
466457
#

biscuit_test.py

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from biscuit_auth import KeyPair, Authorizer, AuthorizerBuilder, Biscuit, BiscuitBuilder, BlockBuilder, Check, Fact, KeyPair, Policy, PrivateKey, PublicKey, Rule, UnverifiedBiscuit
7+
from biscuit_auth import Algorithm, KeyPair, Authorizer, AuthorizerBuilder, Biscuit, BiscuitBuilder, BlockBuilder, Check, Fact, KeyPair, Policy, PrivateKey, PublicKey, Rule, UnverifiedBiscuit
88

99
def test_fact():
1010
fact = Fact('fact(1, true, "", "Test", hex:aabbcc, 2023-04-29T01:00:00Z)')
@@ -13,7 +13,7 @@ def test_fact():
1313

1414
def test_biscuit_builder():
1515
kp = KeyPair()
16-
pubkey = PublicKey.from_hex("acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
16+
pubkey = PublicKey("ed25519/acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
1717

1818
builder = BiscuitBuilder(
1919
"""
@@ -81,7 +81,7 @@ def test_biscuit_builder():
8181
assert True
8282

8383
def test_block_builder():
84-
pubkey = PublicKey.from_hex("acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
84+
pubkey = PublicKey("ed25519/acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
8585
builder = BlockBuilder(
8686
"""
8787
string({str});
@@ -134,7 +134,7 @@ def test_block_builder():
134134
"""
135135

136136
def test_authorizer_builder():
137-
pubkey = PublicKey.from_hex("acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
137+
pubkey = PublicKey("ed25519/acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
138138
builder = AuthorizerBuilder(
139139
"""
140140
string({str});
@@ -207,7 +207,7 @@ def test_authorizer_limits():
207207

208208

209209
def test_key_selection():
210-
private_key = PrivateKey.from_hex("473b5189232f3f597b5c2f3f9b0d5e28b1ee4e7cce67ec6b7fbf5984157a6b97")
210+
private_key = PrivateKey("ed25519/473b5189232f3f597b5c2f3f9b0d5e28b1ee4e7cce67ec6b7fbf5984157a6b97")
211211
root = KeyPair.from_private_key(private_key)
212212
other_root = KeyPair()
213213

@@ -238,7 +238,7 @@ def choose_key(kid):
238238
pass
239239

240240
def test_complete_lifecycle():
241-
private_key = PrivateKey.from_hex("473b5189232f3f597b5c2f3f9b0d5e28b1ee4e7cce67ec6b7fbf5984157a6b97")
241+
private_key = PrivateKey("ed25519/473b5189232f3f597b5c2f3f9b0d5e28b1ee4e7cce67ec6b7fbf5984157a6b97")
242242
root = KeyPair.from_private_key(private_key)
243243

244244
biscuit_builder = BiscuitBuilder("user({id})", { 'id': "1234" })
@@ -267,7 +267,7 @@ def test_complete_lifecycle():
267267
assert facts[0].terms == ["1234"]
268268

269269
def test_snapshot():
270-
private_key = PrivateKey.from_hex("473b5189232f3f597b5c2f3f9b0d5e28b1ee4e7cce67ec6b7fbf5984157a6b97")
270+
private_key = PrivateKey("ed25519/473b5189232f3f597b5c2f3f9b0d5e28b1ee4e7cce67ec6b7fbf5984157a6b97")
271271
root = KeyPair.from_private_key(private_key)
272272

273273
biscuit_builder = BiscuitBuilder("user({id})", { 'id': "1234" })
@@ -318,46 +318,46 @@ def test_snapshot():
318318

319319
def test_public_keys():
320320
# Happy path (hex to bytes and back)
321-
public_key_from_hex = PublicKey.from_hex("acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
321+
public_key_from_hex = PublicKey("ed25519/acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
322322
public_key_bytes = bytes(public_key_from_hex.to_bytes())
323-
public_key_from_bytes = PublicKey.from_bytes(public_key_bytes)
324-
assert public_key_from_bytes.to_hex() == "acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189"
323+
public_key_from_bytes = PublicKey.from_bytes(public_key_bytes, Algorithm.Ed25519)
324+
assert repr(public_key_from_bytes) == "ed25519/acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189"
325325

326326
# Not valid hex
327327
with pytest.raises(ValueError):
328-
PublicKey.from_hex("notarealkey")
328+
PublicKey("notarealkey")
329329

330330
# Valid hex, but too short
331331
with pytest.raises(ValueError):
332-
PublicKey.from_hex("deadbeef1234")
332+
PublicKey("ed25519/deadbeef1234")
333333

334334
# Not enough bytes
335335
with pytest.raises(ValueError):
336-
PublicKey.from_bytes(b"1230fw9ia3")
336+
PublicKey.from_bytes(b"1230fw9ia3", Algorithm.Ed25519)
337337

338338
def test_private_keys():
339339
# Happy path (hex to bytes and back)
340-
private_key_from_hex = PrivateKey.from_hex("12aca40167fbdd1a11037e9fd440e3d510d9d9dea70a6646aa4aaf84d718d75a")
340+
private_key_from_hex = PrivateKey("ed25519/12aca40167fbdd1a11037e9fd440e3d510d9d9dea70a6646aa4aaf84d718d75a")
341341
private_key_bytes = bytes(private_key_from_hex.to_bytes())
342-
private_key_from_bytes = PrivateKey.from_bytes(private_key_bytes)
343-
assert private_key_from_bytes.to_hex() == "12aca40167fbdd1a11037e9fd440e3d510d9d9dea70a6646aa4aaf84d718d75a"
342+
private_key_from_bytes = PrivateKey.from_bytes(private_key_bytes, Algorithm.Ed25519)
343+
assert repr(private_key_from_bytes) == "ed25519/12aca40167fbdd1a11037e9fd440e3d510d9d9dea70a6646aa4aaf84d718d75a"
344344

345345
# Not valid hex
346346
with pytest.raises(ValueError):
347-
PrivateKey.from_hex("notarealkey")
347+
PrivateKey("notarealkey")
348348

349349
# Valid hex, but too short
350350
with pytest.raises(ValueError):
351-
PrivateKey.from_hex("deadbeef1234")
351+
PrivateKey("ed25519/deadbeef1234")
352352

353353
# Not enough bytes
354354
with pytest.raises(ValueError):
355-
PrivateKey.from_bytes(b"1230fw9ia3")
355+
PrivateKey.from_bytes(b"1230fw9ia3", Algorithm.Ed25519)
356356

357357

358358
def test_biscuit_inspection():
359359
kp = KeyPair()
360-
pubkey = PublicKey.from_hex("acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
360+
pubkey = PublicKey("ed25519/acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
361361

362362
builder = BiscuitBuilder(
363363
"""
@@ -391,7 +391,7 @@ def test_biscuit_inspection():
391391
assert utoken2.block_source(1) == "test(false);\n"
392392

393393
def test_biscuit_revocation_ids():
394-
public_key = PublicKey.from_hex("acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
394+
public_key = PublicKey("ed25519/acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
395395

396396
token = Biscuit.from_base64("Ep8BCjUKB3VzZXJfaWQKBWFsaWNlCgVmaWxlMRgDIgoKCAiACBIDGIEIIg4KDAgHEgMYgQgSAxiCCBIkCAASINFHns5iUW6aZiXA0GoqXyrpvFXrquiRGBZjPy3VJPoHGkAC0oew5bIngBkvg1FThYPBf30CAOBksyofzweJnmT_sQ5N4yT1xevHLImmPkJDFyJs9VXrQtroGy_UY5z3WREIGsgBCl4KATAKATEYAyouCgsIBBIDCIMIEgIYABIHCAISAwiDCBIICIAIEgMIhAgSDAgHEgMIhAgSAwiDCDIkCiIKAggbEgcIAhIDCIMIEgYIAxICGAASCwgEEgMIgwgSAhgAEiQIABIg2QCord1Cw30xC2Oea3AuAOPZ2Mvm9-EQmV3zN7zXwQEaQCLnXqIAz3srYrOJKY_g3slzt_nH5U52w8QYEdcuqCxoInvJB5t9BZht4X75MBzM3Aj1AjRVOGmH0ebuQ5GxnwYagwEKGQoFZmlsZTIYAyIOCgwIBxIDGIEIEgMYhQgSJAgAEiAMOoeV68xL1RTh_y4VeK3DUDBP_gnlPSsckzo87Pf7ihpAFAo2Mf7K5VC1HlC5uCK5R_tIXIAHCzRIL6EWzepWAUAWSh0KlZtA_tinJ-L2LAtXY1dgxIjIvw7agO5ZFVjECSIiCiBuSznFYC0NJn8VmDlZmiq1GpBSOERAwHjLZoQJG_24NA==", public_key)
397397
utoken = UnverifiedBiscuit.from_base64("Ep8BCjUKB3VzZXJfaWQKBWFsaWNlCgVmaWxlMRgDIgoKCAiACBIDGIEIIg4KDAgHEgMYgQgSAxiCCBIkCAASINFHns5iUW6aZiXA0GoqXyrpvFXrquiRGBZjPy3VJPoHGkAC0oew5bIngBkvg1FThYPBf30CAOBksyofzweJnmT_sQ5N4yT1xevHLImmPkJDFyJs9VXrQtroGy_UY5z3WREIGsgBCl4KATAKATEYAyouCgsIBBIDCIMIEgIYABIHCAISAwiDCBIICIAIEgMIhAgSDAgHEgMIhAgSAwiDCDIkCiIKAggbEgcIAhIDCIMIEgYIAxICGAASCwgEEgMIgwgSAhgAEiQIABIg2QCord1Cw30xC2Oea3AuAOPZ2Mvm9-EQmV3zN7zXwQEaQCLnXqIAz3srYrOJKY_g3slzt_nH5U52w8QYEdcuqCxoInvJB5t9BZht4X75MBzM3Aj1AjRVOGmH0ebuQ5GxnwYagwEKGQoFZmlsZTIYAyIOCgwIBxIDGIEIEgMYhQgSJAgAEiAMOoeV68xL1RTh_y4VeK3DUDBP_gnlPSsckzo87Pf7ihpAFAo2Mf7K5VC1HlC5uCK5R_tIXIAHCzRIL6EWzepWAUAWSh0KlZtA_tinJ-L2LAtXY1dgxIjIvw7agO5ZFVjECSIiCiBuSznFYC0NJn8VmDlZmiq1GpBSOERAwHjLZoQJG_24NA==")
@@ -408,7 +408,7 @@ def test_biscuit_revocation_ids():
408408
]
409409

410410
def test_unverified_biscuit_signature_check():
411-
public_key = PublicKey.from_hex("acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
411+
public_key = PublicKey("ed25519/acdd6d5b53bfee478bf689f8e012fe7988bf755e3d7c5152947abc149bc20189")
412412

413413
utoken = UnverifiedBiscuit.from_base64("Ep8BCjUKB3VzZXJfaWQKBWFsaWNlCgVmaWxlMRgDIgoKCAiACBIDGIEIIg4KDAgHEgMYgQgSAxiCCBIkCAASINFHns5iUW6aZiXA0GoqXyrpvFXrquiRGBZjPy3VJPoHGkAC0oew5bIngBkvg1FThYPBf30CAOBksyofzweJnmT_sQ5N4yT1xevHLImmPkJDFyJs9VXrQtroGy_UY5z3WREIGsgBCl4KATAKATEYAyouCgsIBBIDCIMIEgIYABIHCAISAwiDCBIICIAIEgMIhAgSDAgHEgMIhAgSAwiDCDIkCiIKAggbEgcIAhIDCIMIEgYIAxICGAASCwgEEgMIgwgSAhgAEiQIABIg2QCord1Cw30xC2Oea3AuAOPZ2Mvm9-EQmV3zN7zXwQEaQCLnXqIAz3srYrOJKY_g3slzt_nH5U52w8QYEdcuqCxoInvJB5t9BZht4X75MBzM3Aj1AjRVOGmH0ebuQ5GxnwYagwEKGQoFZmlsZTIYAyIOCgwIBxIDGIEIEgMYhQgSJAgAEiAMOoeV68xL1RTh_y4VeK3DUDBP_gnlPSsckzo87Pf7ihpAFAo2Mf7K5VC1HlC5uCK5R_tIXIAHCzRIL6EWzepWAUAWSh0KlZtA_tinJ-L2LAtXY1dgxIjIvw7agO5ZFVjECSIiCiBuSznFYC0NJn8VmDlZmiq1GpBSOERAwHjLZoQJG_24NA==")
414414

@@ -423,13 +423,13 @@ def test_append_on_unverified():
423423

424424
def test_keypair_from_private_key_der():
425425
private_key_der = bytes.fromhex("302e020100300506032b6570042204200499694d0da05dcac40052663e71d50c1539465f8926dfe92033cf7aaad53d65")
426-
private_key_hex = "0499694d0da05dcac40052663e71d50c1539465f8926dfe92033cf7aaad53d65"
427-
kp = KeyPair.from_private_key_der(der=private_key_der)
428-
assert kp.private_key.to_hex() == private_key_hex
426+
private_key_hex = "ed25519/0499694d0da05dcac40052663e71d50c1539465f8926dfe92033cf7aaad53d65"
427+
kp = KeyPair.from_private_key(PrivateKey.from_der(der=private_key_der))
428+
assert repr(kp.private_key) == private_key_hex
429429

430430

431431
def test_keypair_from_private_key_pem():
432432
private_key_pem = "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIASZaU0NoF3KxABSZj5x1QwVOUZfiSbf6SAzz3qq1T1l\n-----END PRIVATE KEY-----"
433-
private_key_hex = "0499694d0da05dcac40052663e71d50c1539465f8926dfe92033cf7aaad53d65"
434-
kp = KeyPair.from_private_key_pem(pem=private_key_pem)
435-
assert kp.private_key.to_hex() == private_key_hex
433+
private_key_hex = "ed25519/0499694d0da05dcac40052663e71d50c1539465f8926dfe92033cf7aaad53d65"
434+
kp = KeyPair.from_private_key(PrivateKey.from_pem(pem=private_key_pem))
435+
assert repr(kp.private_key) == private_key_hex

docs/basic-use.rst

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,23 @@ Create and manage keypairs
1111
>>> # random keypair
1212
>>> keypair = KeyPair()
1313
>>> # serialize a keypair to hexadecimal strings
14-
>>> private_key_str = keypair.private_key.to_hex()
15-
>>> public_key_str = keypair.public_key.to_hex()
14+
>>> private_key_str = repr(keypair.private_key)
15+
>>> public_key_str = repr(keypair.public_key)
1616
>>> # parse a private key from an hex string
17-
>>> parsed_private_key = PrivateKey.from_hex("23d9d45b32899eefd4cde9a2caecdd41f0449c95ee1e4c6b53ef38cb957dd690")
17+
>>> parsed_private_key = PrivateKey("ed25519/23d9d45b32899eefd4cde9a2caecdd41f0449c95ee1e4c6b53ef38cb957dd690")
1818
>>> # parse a public key from an hex string
19-
>>> parsed_public_key = PublicKey.from_hex("9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69")
19+
>>> parsed_public_key = PublicKey("ed25519/9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69")
2020
>>> # build a keypair from a private key
2121
>>> parsed_keypair = KeyPair.from_private_key(parsed_private_key)
22-
>>> parsed_keypair.private_key.to_hex()
23-
'23d9d45b32899eefd4cde9a2caecdd41f0449c95ee1e4c6b53ef38cb957dd690'
24-
>>> parsed_keypair.public_key.to_hex()
25-
'9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69'
22+
>>> parsed_keypair.private_key
23+
ed25519/23d9d45b32899eefd4cde9a2caecdd41f0449c95ee1e4c6b53ef38cb957dd690
24+
>>> parsed_keypair.public_key
25+
ed25519/9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69
2626

2727
Build a biscuit token
2828
---------------------
2929

30-
>>> private_key = PrivateKey.from_hex("23d9d45b32899eefd4cde9a2caecdd41f0449c95ee1e4c6b53ef38cb957dd690")
30+
>>> private_key = PrivateKey("ed25519/23d9d45b32899eefd4cde9a2caecdd41f0449c95ee1e4c6b53ef38cb957dd690")
3131
>>> token = BiscuitBuilder("""
3232
... user({user_id});
3333
... check if time($time), $time < {expiration};
@@ -41,7 +41,7 @@ Build a biscuit token
4141

4242
Biscuit tokens can carry a root key identifier, helping the verifying party select the correct public key amongst several valid keys. This is especially useful when performing key rotation, when multiple keys are active at the same time.
4343

44-
>>> private_key = PrivateKey.from_hex("00731a0f129f088e069d8a8b3523a724bc48136bfc22c916cb754adbf503ad5e")
44+
>>> private_key = PrivateKey("ed25519/00731a0f129f088e069d8a8b3523a724bc48136bfc22c916cb754adbf503ad5e")
4545
>>> builder = BiscuitBuilder("""
4646
... user({user_id});
4747
... check if time($time), $time < {expiration};
@@ -70,7 +70,7 @@ Append a block to a biscuit token
7070
Parse and authorize a biscuit token
7171
-----------------------------------
7272

73-
>>> public_key = PublicKey.from_hex("9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69")
73+
>>> public_key = PublicKey("ed25519/9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69")
7474
>>> token = Biscuit.from_base64("En0KEwoEMTIzNBgDIgkKBwgKEgMYgAgSJAgAEiCp8D9laR_CXmFmiUlo6zi8L63iapXDxX1evELp4HVaBRpAx3Mkwu2f2AcNq48IZwu-pxACq1stL76DSMGEugmiduuTVwMqLmgKZ4VFgzeydCrYY_Id3MkxgTgjXzEHUH4DDSIiCiB55I7ykL9wQXHRDqUnSgZwCdYNdO7c8LZEj0VH5sy3-Q==", public_key)
7575
>>> authorizer = AuthorizerBuilder( """ time({now}); allow if user($u); """, { 'now': datetime.now(tz = timezone.utc)} ).build(token)
7676
>>> authorizer.authorize()
@@ -80,9 +80,9 @@ In order to help with key rotation, biscuit tokens can optionally carry a root k
8080

8181
>>> def public_key_fn(kid):
8282
... if kid is None:
83-
... return PublicKey.from_hex("9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69")
83+
... return PublicKey("ed25519/9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69")
8484
... elif kid == 1:
85-
... return PublicKey.from_hex("1d211ddaf521cc45b620431817ba4fe0457be467ba4d724ecf514db3070b53cc")
85+
... return PublicKey("ed25519/1d211ddaf521cc45b620431817ba4fe0457be467ba4d724ecf514db3070b53cc")
8686
... else:
8787
... raise Exception("unknown key identifier")
8888
>>> token = Biscuit.from_base64("CAESfQoTCgQxMjM0GAMiCQoHCAoSAxiACBIkCAASII5WVsvM52T91C12wnzButmyzmtGSX_rbM6hCSIJihX2GkDwAcVxTnY8aeMLm-i2R_VzTfIMQZya49ogXO2h2Fg2TJsDcG3udIki9il5PA05lKUwrfPNroS7Qg5e04AyLLcHIiIKII5rh75jrCrgE6Rzw6GVYczMn1IOo287uO4Ef5wp7obY", public_key_fn)

docs/datalog.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Rules, checks and policies can also contain parameters for public keys. Those ar
3434
... check if admin({user}) trusting {rights_service_pubkey}
3535
... """,
3636
... {'user': "abcd" },
37-
... {'rights_service_pubkey': PublicKey.from_hex("9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69") })
37+
... {'rights_service_pubkey': PublicKey("ed25519/9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69") })
3838
check if admin("abcd") trusting ed25519/9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69;
3939
<BLANKLINE>
4040

0 commit comments

Comments
 (0)