From 81ffcd60f5d3b9320fc28ee335fbbe2e4c7373bb Mon Sep 17 00:00:00 2001 From: Ivar Date: Thu, 22 Aug 2024 15:37:41 +0100 Subject: [PATCH] new cli argument allowing to specify receipt type to get after submission --- DEVELOPMENT.md | 29 +++++-- README.md | 2 +- demo/github/4-submit.sh | 2 - pyscitt/pyscitt/cli/submit_signed_claims.py | 38 +++++++-- pyscitt/pyscitt/client.py | 69 +++++++++++----- pyscitt/pyscitt/crypto.py | 18 +++++ pyscitt/pyscitt/verify.py | 8 +- test/load_test/locustfile.py | 6 +- test/test_auth.py | 2 +- test/test_ccf.py | 10 ++- test/test_cli.py | 87 +++++++++++++++++++++ test/test_configuration.py | 40 +++++----- test/test_encoding.py | 8 +- test/test_faketime.py | 2 +- test/test_historical.py | 2 +- test/test_operations.py | 6 +- test/test_perf.py | 16 ++-- test/test_prefix_tree.py | 6 +- test/test_resolution.py | 37 ++++----- test/test_x509.py | 24 +++--- 20 files changed, 291 insertions(+), 121 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index feefc001..c8a3077d 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -132,18 +132,37 @@ Root CAs are used to validate COSE envelopes being submitted to the `/entries` e scitt-ccf-ledger has unit tests, covering individual components of the source code, and functional tests, covering end-to-end use cases of scitt-ccf-ledger. -The unit tests can be run with: +### Unit tests + +The unit tests can be run with `run_unit_tests.sh` script. ```sh +PLATFORM="virtual" ./docker/build.sh ./run_unit_tests.sh ``` -All functional tests can be run with: +### Functional (e2e) tests -```sh -./run_functional_tests.sh -``` +To start the tests you need to use the script `run_functional_tests.sh`. Specific functional test can also be run by passing additional `pytest` arguments, e.g. `./run_functional_tests.sh -k test_use_cacert_submit_verify_x509_signature` Note: the functional tests will launch their own CCF network on a randomly assigned port. You do not need to start an instance beforehand. + +**Using Docker** + +The script will launch the built Docker image and will execute tests against it: + +```sh +PLATFORM="virtual" ./docker/build.sh +DOCKER=1 PLATFORM=virtual ./run_functional_tests.sh +``` + +**Using your host environment** + +```sh +PLATFORM=virtual ./build.sh +PLATFORM=virtual ./run_functional_tests.sh +``` + + diff --git a/README.md b/README.md index b0b4900f..d57b06f5 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ See the `demo/` folder on how to interact with the application. ### Development setup -See [DEVELOPMENT.md](DEVELOPMENT.md) for instructions on building, running, and testing scitt-ccf-ledger without Docker. +See [DEVELOPMENT.md](DEVELOPMENT.md) for instructions on building, running, and testing scitt-ccf-ledger. ### Using the CLI diff --git a/demo/github/4-submit.sh b/demo/github/4-submit.sh index 0033e5ad..9cf24c9c 100755 --- a/demo/github/4-submit.sh +++ b/demo/github/4-submit.sh @@ -5,12 +5,10 @@ set -ex SCITT_URL=${SCITT_URL:-"https://127.0.0.1:8000"} -SCITT_TRUST_STORE=tmp/trust_store TMP_DIR=tmp/github scitt submit $TMP_DIR/claims.cose \ --receipt $TMP_DIR/claims.receipt.cbor \ --url "$SCITT_URL" \ - --service-trust-store $SCITT_TRUST_STORE \ --development diff --git a/pyscitt/pyscitt/cli/submit_signed_claims.py b/pyscitt/pyscitt/cli/submit_signed_claims.py index 1c7eb15a..341ad357 100644 --- a/pyscitt/pyscitt/cli/submit_signed_claims.py +++ b/pyscitt/pyscitt/cli/submit_signed_claims.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Optional -from ..client import Client +from ..client import Client, ReceiptType from ..verify import StaticTrustStore, verify_receipt from .client_arguments import add_client_arguments, create_client @@ -14,27 +14,39 @@ def submit_signed_claimset( client: Client, path: Path, receipt_path: Optional[Path], + receipt_type: str, service_trust_store_path: Optional[Path], skip_confirmation: bool, ): if path.suffix != ".cose": - raise ValueError("unsupported file extension") + raise ValueError("unsupported file extension, must end with .cose") + + if receipt_type == "raw": + r_type = ReceiptType.RAW + elif receipt_type == "embedded": + r_type = ReceiptType.EMBEDDED + else: + raise ValueError(f"unsupported receipt type {receipt_type}") with open(path, "rb") as f: signed_claimset = f.read() if skip_confirmation: - pending = client.submit_claim(signed_claimset, skip_confirmation=True) + pending = client.submit_claim(signed_claimset) print(f"Submitted {path} as operation {pending.operation_tx}") - print("Confirmation of submission was skipped! Claim may not be registered.") + print( + """Confirmation of submission was skipped! + There is a small chance the claim may not be registered. + Receipt will not be downloaded and saved.""" + ) return - submission = client.submit_claim(signed_claimset) + submission = client.submit_claim_and_confirm(signed_claimset, receipt_type=r_type) print(f"Submitted {path} as transaction {submission.tx}") if receipt_path: with open(receipt_path, "wb") as f: - f.write(submission.raw_receipt) + f.write(submission.receipt_bytes) print(f"Received {receipt_path}") if service_trust_store_path: @@ -48,10 +60,10 @@ def submit_signed_claimset( def cli(fn): parser = fn( - description="Submit signed claimset to a SCITT CCF Ledger and retrieve receipt" + description="Submit signed claimset (COSE) to a SCITT CCF Ledger and retrieve receipt" ) add_client_arguments(parser, with_auth_token=True) - parser.add_argument("path", type=Path, help="Path to signed claimset file") + parser.add_argument("path", type=Path, help="Path to signed claimset file (COSE)") group = parser.add_mutually_exclusive_group() group.add_argument("--receipt", type=Path, help="Output path to receipt file") group.add_argument( @@ -59,6 +71,15 @@ def cli(fn): action="store_true", help="Don't wait for confirmation or a receipt", ) + parser.add_argument( + "--receipt-type", + choices=["embedded", "raw"], + default="raw", # default to raw for backwards compatibility + help=""" + Downloads the receipt of a given type where raw means a countersignature (CBOR) binary + and embedded means the original claimset (COSE) with the raw receipt added to the unprotected header + """, + ) parser.add_argument( "--service-trust-store", type=Path, @@ -71,6 +92,7 @@ def cmd(args): client, args.path, args.receipt, + args.receipt_type, args.service_trust_store, args.skip_confirmation, ) diff --git a/pyscitt/pyscitt/client.py b/pyscitt/pyscitt/client.py index f076db76..23b3b810 100644 --- a/pyscitt/pyscitt/client.py +++ b/pyscitt/pyscitt/client.py @@ -35,6 +35,13 @@ class SigningType(Enum): HTTP = "HTTP" +class ReceiptType(Enum): + """Receipt types supported by the ledger.""" + + EMBEDDED = "EMBEDDED" + RAW = "RAW" + + class MemberAuthenticationMethod(ABC): cert: str @@ -482,7 +489,7 @@ def get_historical(self, *args, retry_on=[], **kwargs): @dataclass class PendingSubmission: """ - The result of submitting a claim to the service. + The pending result of submitting a claim to the service. """ operation_tx: str @@ -490,17 +497,31 @@ class PendingSubmission: @dataclass class Submission(PendingSubmission): + """ + The result of submitting a claim to the service. + """ + tx: str - raw_receipt: bytes + receipt_bytes: bytes + is_receipt_embedded: bool @property def seqno(self) -> int: + """Extract the sequence number from the transaction ID.""" view, seqno = self.tx.split(".") return int(seqno) @property def receipt(self) -> Receipt: - return Receipt.decode(self.raw_receipt) + """Parse the receipt bytes and return a Receipt object.""" + if self.is_receipt_embedded: + embedded_receipt = crypto.get_last_embedded_receipt_from_cose( + self.receipt_bytes + ) + if embedded_receipt: + return Receipt.decode(embedded_receipt) + raise ValueError("No embedded receipt found in COSE message header") + return Receipt.decode(self.receipt_bytes) class Client(BaseClient): @@ -523,33 +544,39 @@ def get_did_document(self, did: str) -> dict: # Note: This endpoint only returns data for did:web DIDs. return self.get(f"/did/{did}").json()["did_document"] - @overload def submit_claim( - self, claim: bytes, *, skip_confirmation: Literal[False] = False - ) -> Submission: ... - - @overload - def submit_claim( - self, claim: bytes, *, skip_confirmation: Literal[True] - ) -> PendingSubmission: ... - - def submit_claim( - self, claim: bytes, *, skip_confirmation=False - ) -> Union[Submission, PendingSubmission]: + self, + claim: bytes, + ) -> PendingSubmission: headers = {"Content-Type": "application/cose"} response = self.post( "/entries", headers=headers, content=claim, ).json() + operation_id = response["operationId"] + return PendingSubmission(operation_id) + def submit_claim_and_confirm( + self, + claim: bytes, + *, + receipt_type: ReceiptType = ReceiptType.RAW, + ) -> Submission: + headers = {"Content-Type": "application/cose"} + response = self.post( + "/entries", + headers=headers, + content=claim, + ).json() operation_id = response["operationId"] - if skip_confirmation: - return PendingSubmission(operation_id) - else: - tx = self.wait_for_operation(operation_id) - receipt = self.get_receipt(tx, decode=False) - return Submission(operation_id, tx, receipt) + tx = self.wait_for_operation(operation_id) + if receipt_type == ReceiptType.EMBEDDED: + receipt = self.get_claim(tx, embed_receipt=True) + return Submission(operation_id, tx, receipt, True) + + receipt = self.get_receipt(tx, decode=False) + return Submission(operation_id, tx, receipt, False) def wait_for_operation(self, operation: str) -> str: response = self.get( diff --git a/pyscitt/pyscitt/crypto.py b/pyscitt/pyscitt/crypto.py index 04ce96c2..1a768ed2 100644 --- a/pyscitt/pyscitt/crypto.py +++ b/pyscitt/pyscitt/crypto.py @@ -574,6 +574,7 @@ def sha256_file(path: Path) -> str: def embed_receipt_in_cose(buf: bytes, receipt: bytes) -> bytes: + """Append the receipt to an unprotected header in a COSE_Sign1 message.""" # Need to parse the receipt to avoid wrapping it in a bstr. parsed_receipt = cbor2.loads(receipt) @@ -591,6 +592,23 @@ def embed_receipt_in_cose(buf: bytes, receipt: bytes) -> bytes: return cbor2.dumps(outer) +def get_last_embedded_receipt_from_cose(buf: bytes) -> Union[bytes, None]: + """Extract the last receipt from the unprotected header of a COSE_Sign1 message.""" + outer = cbor2.loads(buf) + if hasattr(outer, "tag"): + assert outer.tag == 18 # COSE_Sign1 + val = outer.value # type: ignore[attr-defined] + else: + val = outer + [_, uhdr, _, _] = val + key = COSE_HEADER_PARAM_SCITT_RECEIPTS + if key in uhdr: + parsed_receipts = uhdr[key] + if isinstance(parsed_receipts, list) and parsed_receipts: + return cbor2.dumps(parsed_receipts[-1]) + return None + + def load_private_key(key_path: Path) -> Pem: with open(key_path) as f: key_priv_pem = f.read() diff --git a/pyscitt/pyscitt/verify.py b/pyscitt/pyscitt/verify.py index d63b8410..05c759ab 100644 --- a/pyscitt/pyscitt/verify.py +++ b/pyscitt/pyscitt/verify.py @@ -91,10 +91,10 @@ def verify_receipt( decoded_receipt = receipt else: if receipt is None: - parsed_receipts = msg.uhdr[COSE_HEADER_PARAM_SCITT_RECEIPTS] - # For now, assume there is only one receipt - assert len(parsed_receipts) == 1 - parsed_receipt = parsed_receipts[0] + embedded_receipt = crypto.get_last_embedded_receipt_from_cose(buf) + if embedded_receipt is None: + raise ValueError("No embedded receipt found in COSE message") + parsed_receipt = cbor2.loads(embedded_receipt) else: parsed_receipt = cbor2.loads(receipt) diff --git a/test/load_test/locustfile.py b/test/load_test/locustfile.py index ab383304..8d9ba847 100644 --- a/test/load_test/locustfile.py +++ b/test/load_test/locustfile.py @@ -59,7 +59,9 @@ def submit_claim(self): claim = self._claims[random.randrange(len(self._claims))] self.trace( "submit_claim", - lambda: self.client.submit_claim( - claim, skip_confirmation=self.skip_confirmation + lambda: ( + self.client.submit_claim(claim) + if self.skip_confirmation + else self.client.submit_claim_and_confirm(claim) ), ) diff --git a/test/test_auth.py b/test/test_auth.py index 3eff89c8..228f76a4 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -43,7 +43,7 @@ def f(*, allow_unauthenticated: bool, required_claims=None): @pytest.fixture def submit(self, client: Client, claim: bytes): def f(**kwargs): - client.replace(**kwargs).submit_claim(claim) + client.replace(**kwargs).submit_claim_and_confirm(claim) return f diff --git a/test/test_ccf.py b/test/test_ccf.py index ea9c1179..aef8b256 100644 --- a/test/test_ccf.py +++ b/test/test_ccf.py @@ -34,7 +34,7 @@ def test_submit_claim(client: Client, did_web, trust_store, params): # Sign and submit a dummy claim using our new identity claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) - receipt = client.submit_claim(claims).raw_receipt + receipt = client.submit_claim_and_confirm(claims).receipt_bytes verify_receipt(claims, trust_store, receipt) embedded = crypto.embed_receipt_in_cose(claims, receipt) @@ -72,14 +72,14 @@ def test_default_did_port(client: Client, trust_store, tmp_path): # Sign and submit a dummy claim using our new identity claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) - receipt = client.submit_claim(claims).receipt + receipt = client.submit_claim_and_confirm(claims).receipt verify_receipt(claims, trust_store, receipt) @pytest.mark.isolated_test def test_recovery(client, did_web, restart_service): identity = did_web.create_identity() - client.submit_claim(crypto.sign_json_claimset(identity, {"foo": "bar"})) + client.submit_claim_and_confirm(crypto.sign_json_claimset(identity, {"foo": "bar"})) old_network = client.get("/node/network").json() assert old_network["recovery_count"] == 0 @@ -91,4 +91,6 @@ def test_recovery(client, did_web, restart_service): assert new_network["service_certificate"] != old_network["service_certificate"] # Check that the service is still operating correctly - client.submit_claim(crypto.sign_json_claimset(identity, {"foo": "hello"})) + client.submit_claim_and_confirm( + crypto.sign_json_claimset(identity, {"foo": "hello"}) + ) diff --git a/test/test_cli.py b/test/test_cli.py index 90e204ed..65d8f3e8 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -241,6 +241,93 @@ def test_use_cacert_submit_verify_x509_signature(run, client, tmp_path: Path): ) +def test_use_cacert_submit_verify_x509_embedded(run, client, tmp_path: Path): + # Add basic service config + (tmp_path / "config.json").write_text( + json.dumps({"authentication": {"allow_unauthenticated": True}}) + ) + run( + "governance", + "propose_configuration", + "--configuration", + tmp_path / "config.json", + with_service_url=True, + with_member_auth=True, + ) + + # Get the CA cert from the service params + # Once in production this value can come from other trusted places + service_params = client.get_parameters().as_dict() + (tmp_path / "tlscacert.pem").write_text( + f"-----BEGIN CERTIFICATE-----\n{service_params.get('serviceCertificate')}\n-----END CERTIFICATE-----\n" + ) + + # Setup signing keys imitating how third party might do it + generate_ca_cert_and_key( + f"{tmp_path}", + "ES256", + "ec", + "P-256", + key_filename="signerkey.pem", + cacert_filename="signerca.pem", + ) + + # Configure SCITT policy to accept the message if it was signed + # by the given key + run( + "governance", + "propose_ca_certs", + "--name", + "x509_roots", + "--ca-certs", + tmp_path / "signerca.pem", + with_service_url=True, + with_member_auth=True, + ) + + # Prepare an x509 cose file to submit to the service + (tmp_path / "claims.json").write_text(json.dumps({"foo": "bar"})) + run( + "sign", + "--key", + tmp_path / "signerkey.pem", + "--claims", + tmp_path / "claims.json", + "--content-type", + "application/json", + "--x5c", + tmp_path / "signerca.pem", + "--out", + tmp_path / "claims.cose", + ) + + # Submit cose and make sure TLS verification is enabled + # this should exit without error + run( + "submit", + "--cacert", + tmp_path / "tlscacert.pem", + tmp_path / "claims.cose", + "--url", + # TLS cert SAN entries come from config node_certificate.subject_alt_names + client.url, + "--receipt", + tmp_path / "claims.embedded.cose", + "--receipt-type", + "embedded", + ) + + trust_store_path = tmp_path / "store" + trust_store_path.mkdir() + (trust_store_path / "service.json").write_text(json.dumps(service_params)) + run( + "validate", + tmp_path / "claims.embedded.cose", + "--service-trust-store", + trust_store_path, + ) + + def test_local_development(run, service_url, tmp_path: Path): # This is not particularly useful to run tests against, since it uses Mozilla CA roots, meaning # we can't issue any DID web that would validate, but at least we check that the command doesn't diff --git a/test/test_configuration.py b/test/test_configuration.py index 29c33d47..b5f7fe9e 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -24,7 +24,7 @@ def f(**kwargs): """Sign and submit the claims with a new identity""" identity = did_web.create_identity(**kwargs) claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) return f @@ -78,7 +78,7 @@ def test_reject_all_issuers(self, client: Client, configure_service, claims): configure_service({"policy": {"accepted_did_issuers": []}}) with service_error("InvalidInput: Unsupported DID issuer in protected header"): - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) def test_wrong_accepted_issuer(self, client: Client, configure_service, claims): # Add just one issuer to the policy. Claims signed not with this @@ -86,19 +86,19 @@ def test_wrong_accepted_issuer(self, client: Client, configure_service, claims): configure_service({"policy": {"accepted_did_issuers": ["else"]}}) with service_error("InvalidInput: Unsupported DID issuer in protected header"): - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) def test_allow_any_issuer(self, client: Client, configure_service, claims): # If no accepted_issuers are defined in the policy, any issuers # are accepted. configure_service({"policy": {}}) - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) def test_valid_issuer(self, client: Client, configure_service, identity, claims): # Add just one issuer to the policy. Claims signed with this # issuer are accepted. configure_service({"policy": {"accepted_did_issuers": [identity.issuer]}}) - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) def test_multiple_accepted_issuers( self, client: Client, configure_service, identity, claims @@ -108,7 +108,7 @@ def test_multiple_accepted_issuers( configure_service( {"policy": {"accepted_did_issuers": [identity.issuer, "else"]}} ) - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) class TestPolicyEngine: @@ -262,12 +262,12 @@ def test_x509_policy( configure_service({"policy": {"policy_script": policy_script}}) for signed_claimset in permitted_signed_claims: - client.submit_claim(signed_claimset) + client.submit_claim_and_confirm(signed_claimset) for err, signed_claimsets in refused_signed_claims.items(): for signed_claimset in signed_claimsets: with service_error(err): - client.submit_claim(signed_claimset) + client.submit_claim_and_confirm(signed_claimset) def test_ietf_didx509_policy( self, @@ -386,12 +386,12 @@ def didx509_issuer(ca): configure_service({"policy": {"policy_script": policy_script}}) for signed_claimset in permitted_signed_claims: - client.submit_claim(signed_claimset) + client.submit_claim_and_confirm(signed_claimset) for err, signed_claimsets in refused_signed_claims.items(): for signed_claimset in signed_claimsets: with service_error(err): - client.submit_claim(signed_claimset) + client.submit_claim_and_confirm(signed_claimset) def test_svn_policy( self, @@ -450,12 +450,12 @@ def didx509_issuer(ca): configure_service({"policy": {"policy_script": policy_script}}) for signed_claimset in permitted_signed_claims: - client.submit_claim(signed_claimset) + client.submit_claim_and_confirm(signed_claimset) for err, signed_claimsets in refused_signed_claims.items(): for signed_claimset in signed_claimsets: with service_error(err): - client.submit_claim(signed_claimset) + client.submit_claim_and_confirm(signed_claimset) def test_trivial_pass_policy( self, client: Client, configure_service, signed_claimset @@ -464,7 +464,7 @@ def test_trivial_pass_policy( {"policy": {"policy_script": "export function apply() { return true }"}} ) - client.submit_claim(signed_claimset) + client.submit_claim_and_confirm(signed_claimset) def test_trivial_fail_policy( self, client: Client, configure_service, signed_claimset @@ -478,7 +478,7 @@ def test_trivial_fail_policy( ) with service_error("Policy was not met"): - client.submit_claim(signed_claimset) + client.submit_claim_and_confirm(signed_claimset) def test_exceptional_policy( self, client: Client, configure_service, signed_claimset @@ -492,7 +492,7 @@ def test_exceptional_policy( ) with service_error("Error while applying policy"): - client.submit_claim(signed_claimset) + client.submit_claim_and_confirm(signed_claimset) @pytest.mark.parametrize( "script", @@ -509,7 +509,7 @@ def test_invalid_policy( configure_service({"policy": {"policy_script": script}}) with service_error("Invalid policy module"): - client.submit_claim(signed_claimset) + client.submit_claim_and_confirm(signed_claimset) def test_service_identifier( @@ -521,7 +521,7 @@ def test_service_identifier( claim = crypto.sign_json_claimset(identity, {"foo": "bar"}) # Receipts include an issuer and kid. - receipt = client.submit_claim(claim).receipt + receipt = client.submit_claim_and_confirm(claim).receipt assert receipt.phdr[crypto.COSE_HEADER_PARAM_ISSUER] == service_identifier assert pycose.headers.KID in receipt.phdr @@ -546,7 +546,7 @@ def test_without_service_identifier( assert httpx.get(url, verify=False).status_code == 404 # The receipts it returns have no issuer or kid. - receipt = client.submit_claim(claim).receipt + receipt = client.submit_claim_and_confirm(claim).receipt assert crypto.COSE_HEADER_PARAM_ISSUER not in receipt.phdr assert pycose.headers.KID not in receipt.phdr @@ -587,7 +587,7 @@ def test_did_multiple_service_keys( # Create a claim and get a receipt before the service is restarted. identity = did_web.create_identity() claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) - receipt = client.submit_claim(claims).receipt + receipt = client.submit_claim_and_confirm(claims).receipt verify_receipt(claims, trust_store, receipt) restart_service() @@ -603,5 +603,5 @@ def test_did_multiple_service_keys( # We can also get new receipts, which will use the new identity, and these # can also be verified. - new_receipt = client.submit_claim(claims).receipt + new_receipt = client.submit_claim_and_confirm(claims).receipt verify_receipt(claims, trust_store, new_receipt) diff --git a/test/test_encoding.py b/test/test_encoding.py index 881728b4..65703b8d 100644 --- a/test/test_encoding.py +++ b/test/test_encoding.py @@ -143,7 +143,7 @@ def claim(self, did_web): def test_submit_claim(self, client: Client, trust_store, claim): """The ledger should accept claims even if not canonically encoded.""" - receipt = client.submit_claim(claim).receipt + receipt = client.submit_claim_and_confirm(claim).receipt verify_receipt(claim, trust_store, receipt) def test_embed_receipt(self, client: Client, trust_store, claim): @@ -151,7 +151,7 @@ def test_embed_receipt(self, client: Client, trust_store, claim): When embedding a receipt in a claim, the ledger should not affect the encoding of byte-string pieces. """ - tx = client.submit_claim(claim).tx + tx = client.submit_claim_and_confirm(claim).tx embedded = client.get_claim(tx, embed_receipt=True) original_pieces = cbor2.loads(claim).value # type: ignore[attr-defined] @@ -177,7 +177,7 @@ def test_no_buffer_overflow_when_embedding_receipt(self, client: Client, did_web size = int(1024 * 1024 * 0.5) claim = crypto.sign_claimset(identity, bytes(size), "binary/octet-stream") - tx = client.submit_claim(claim).tx + tx = client.submit_claim_and_confirm(claim).tx embedded = client.get_claim(tx, embed_receipt=True) original_claim_array = cbor2.loads(claim).value # type: ignore[attr-defined] @@ -197,7 +197,7 @@ def identity(self, did_web): @pytest.fixture(scope="class") def submit(self, client, identity): def f(parameters, *, signer=identity): - return client.submit_claim(sign(signer, b"Hello", parameters)) + return client.submit_claim_and_confirm(sign(signer, b"Hello", parameters)) return f diff --git a/test/test_faketime.py b/test/test_faketime.py index 44e32197..eeba0ba7 100644 --- a/test/test_faketime.py +++ b/test/test_faketime.py @@ -39,5 +39,5 @@ def test_faketime( # attested-fetch from starting up. identity = did_web.create_identity() claim = crypto.sign_json_claimset(identity, "Payload") - receipt = client.submit_claim(claim).receipt + receipt = client.submit_claim_and_confirm(claim).receipt verify_receipt(claim, trust_store, receipt) diff --git a/test/test_historical.py b/test/test_historical.py index 55333d2f..abf33908 100644 --- a/test/test_historical.py +++ b/test/test_historical.py @@ -18,7 +18,7 @@ def submissions(self, client: Client, did_web): result = [] for i in range(COUNT): claim = crypto.sign_json_claimset(identity, {"value": i}) - submission = client.submit_claim(claim) + submission = client.submit_claim_and_confirm(claim) result.append( SimpleNamespace( claim=claim, diff --git a/test/test_operations.py b/test/test_operations.py index eeebb89b..8dcfaf72 100644 --- a/test/test_operations.py +++ b/test/test_operations.py @@ -26,10 +26,10 @@ def test_purge_old_operations( # Create two operations, such that one will be old enough to be purged but # not the other. - tx1 = client.submit_claim(claim).operation_tx + tx1 = client.submit_claim_and_confirm(claim).operation_tx cchost.advance_time(seconds=constants.OPERATION_EXPIRY_SECONDS // 2) - tx2 = client.submit_claim(claim).operation_tx + tx2 = client.submit_claim_and_confirm(claim).operation_tx cchost.advance_time(seconds=constants.OPERATION_EXPIRY_SECONDS // 2) # Just moving time forward doesn't actually do anything yet. @@ -40,7 +40,7 @@ def test_purge_old_operations( # Submit a claim again, just to get the indexing strategy # to run. This is what actually triggers a purge. - client.submit_claim(claim) + client.submit_claim_and_confirm(claim) # Now the first operation has actually been purged, but not the second, # since that one is only half as old. diff --git a/test/test_perf.py b/test/test_perf.py index 049d0f85..3148e93f 100644 --- a/test/test_perf.py +++ b/test/test_perf.py @@ -43,11 +43,11 @@ def test_latency(self, client, did_web, trusted_ca): # Test did:web performance (uncached resolution). latency_did_web_uncached_submit_s = measure_latency( - lambda claim: client.submit_claim(claim, skip_confirmation=True), + lambda claim: client.submit_claim(claim), lambda: crypto.sign_json_claimset(did_web.create_identity(), payload), ) latency_did_web_uncached_submit_and_receipt_s = measure_latency( - lambda claim: client.submit_claim(claim, skip_confirmation=False), + lambda claim: client.submit_claim_and_confirm(claim), lambda: crypto.sign_json_claimset(did_web.create_identity(), payload), ) @@ -56,13 +56,13 @@ def test_latency(self, client, did_web, trusted_ca): claim = crypto.sign_json_claimset(identity, payload) # Make sure DID document is cached. - client.submit_claim(claim) + client.submit_claim_and_confirm(claim) latency_did_web_cached_submit_s = measure_latency( - lambda _: client.submit_claim(claim, skip_confirmation=True) + lambda _: client.submit_claim(claim) ) latency_did_web_cached_submit_and_receipt_s = measure_latency( - lambda _: client.submit_claim(claim, skip_confirmation=False) + lambda _: client.submit_claim_and_confirm(claim) ) # Test x5c performance. @@ -71,11 +71,9 @@ def test_latency(self, client, did_web, trusted_ca): ) claim = crypto.sign_json_claimset(identity, payload) - latency_x5c_submit_s = measure_latency( - lambda _: client.submit_claim(claim, skip_confirmation=True) - ) + latency_x5c_submit_s = measure_latency(lambda _: client.submit_claim(claim)) latency_x5c_submit_and_receipt_s = measure_latency( - lambda _: client.submit_claim(claim, skip_confirmation=False) + lambda _: client.submit_claim_and_confirm(claim) ) # Time-to-receipt depends on the configured signature interval of diff --git a/test/test_prefix_tree.py b/test/test_prefix_tree.py index cf0b0690..4961db81 100644 --- a/test/test_prefix_tree.py +++ b/test/test_prefix_tree.py @@ -16,7 +16,7 @@ def test_prefix_tree(did_web, client): service_parameters = client.get_parameters() first_claims = crypto.sign_json_claimset(identity, {"value": "first"}, feed=feed) - first_submit = client.submit_claim(first_claims) + first_submit = client.submit_claim_and_confirm(first_claims) pt = client.prefix_tree @@ -36,7 +36,7 @@ def test_prefix_tree(did_web, client): # Submit a new claim to the same feed. The sequence numbers reflect the submission # ordering. second_claims = crypto.sign_json_claimset(identity, {"value": "second"}, feed=feed) - second_submit = client.submit_claim(second_claims) + second_submit = client.submit_claim_and_confirm(second_claims) assert second_submit.seqno > first_submit.seqno # Get a read receipt again - this will still reference the first claim, @@ -60,7 +60,7 @@ def test_prefix_tree(did_web, client): # We submit a final claim, but to a different feed. # This should not affect read receipts for the original feed. third_claims = crypto.sign_json_claimset(identity, {"value": "third"}, feed="other") - third_submit = client.submit_claim(third_claims) + third_submit = client.submit_claim_and_confirm(third_claims) pt.flush() receipt = pt.get_read_receipt(identity.issuer, feed) diff --git a/test/test_resolution.py b/test/test_resolution.py index 1c76f8c7..f6ae03a2 100644 --- a/test/test_resolution.py +++ b/test/test_resolution.py @@ -32,10 +32,7 @@ def submit_concurrently( def f(claims: List[bytes]) -> List[str]: with did_web.suspend(): - operations = [ - client.submit_claim(c, skip_confirmation=True).operation_tx - for c in claims - ] + operations = [client.submit_claim(c).operation_tx for c in claims] # Give the ledger a second to kick off the requests before we # unblock the DID server. @@ -61,10 +58,10 @@ def test_cache_resolution( with did_web.monitor() as metrics: claim1 = crypto.sign_json_claimset(identity, "Hello") - receipt1 = client.submit_claim(claim1).receipt + receipt1 = client.submit_claim_and_confirm(claim1).receipt claim2 = crypto.sign_json_claimset(identity, "World") - receipt2 = client.submit_claim(claim2).receipt + receipt2 = client.submit_claim_and_confirm(claim2).receipt assert metrics.request_count == 1 verify_receipt(claim1, trust_store, receipt1) @@ -110,11 +107,11 @@ def test_key_rotation( with did_web.monitor() as metrics: identity1 = did_web.create_identity() claim1 = crypto.sign_json_claimset(identity1, "Hello") - receipt1 = client.submit_claim(claim1).receipt + receipt1 = client.submit_claim_and_confirm(claim1).receipt identity2 = did_web.create_identity(identifier=identity1.issuer) claim2 = crypto.sign_json_claimset(identity2, "World") - receipt2 = client.submit_claim(claim2).receipt + receipt2 = client.submit_claim_and_confirm(claim2).receipt assert identity1.issuer == identity2.issuer assert identity1.kid != identity2.kid @@ -124,7 +121,7 @@ def test_key_rotation( # The old claim can't be submitted anymore, since it uses the old key. with service_error("Missing assertion method in DID document"): - client.submit_claim(claim1) + client.submit_claim_and_confirm(claim1) @pytest.mark.isolated_test(enable_faketime=True) @@ -136,14 +133,14 @@ def test_key_expiry(client, did_web, cchost): identity = did_web.create_identity() claim = crypto.sign_json_claimset(identity, "Hello") - client.submit_claim(claim) + client.submit_claim_and_confirm(claim) # Rotate the key did_web.create_identity(identifier=identity.issuer) # The ledger still has the DID document cached, so will accept a claim # signed with the old key. - client.submit_claim(claim) + client.submit_claim_and_confirm(claim) # Time travel, enough for the cached document to expire. cchost.advance_time(seconds=constants.DID_RESOLUTION_CACHE_EXPIRY_SECONDS) @@ -153,7 +150,7 @@ def test_key_expiry(client, did_web, cchost): # now contains a new key. When it does, the document won't contain the old # key anymore and resolution will fail. with service_error("Missing assertion method in DID document"): - client.submit_claim(claim) + client.submit_claim_and_confirm(claim) def test_multiple_keys( @@ -187,7 +184,7 @@ def test_multiple_keys( def submit(private_key, kid, payload): identity = crypto.Signer(private_key, issuer=issuer, kid=kid) claim = crypto.sign_json_claimset(identity, payload) - receipt = client.submit_claim(claim).receipt + receipt = client.submit_claim_and_confirm(claim).receipt verify_receipt(claim, trust_store, receipt) # We can submit claims with either key, using a single DID document. @@ -218,7 +215,7 @@ def test_claim_without_kid( identity = crypto.Signer(issuer=issuer, private_key=private_key) claim = crypto.sign_json_claimset(identity, "Payload") - receipt = client.submit_claim(claim).receipt + receipt = client.submit_claim_and_confirm(claim).receipt verify_receipt(claim, trust_store, receipt) @@ -254,7 +251,7 @@ def test_submit(self, client: Client, did_web: DIDWebServer): claim = crypto.sign_json_claimset(identity, "Payload") with service_error("DID document ID does not match expected value"): - client.submit_claim(claim) + client.submit_claim_and_confirm(claim) def test_submit_concurrently( self, @@ -306,7 +303,7 @@ def test_dont_cache_error( # Submit the claim a first time, with the invalid document with service_error("DID document ID does not match expected value"): - client.submit_claim(claim) + client.submit_claim_and_confirm(claim) # Update the published document. This time it uses the correct DID. document = did.create_document( @@ -316,7 +313,7 @@ def test_dont_cache_error( did_web.write_did_document(document) # Submitting the same claim now works just fine. - receipt = client.submit_claim(claim).receipt + receipt = client.submit_claim_and_confirm(claim).receipt verify_receipt(claim, trust_store, receipt) @@ -334,7 +331,7 @@ def test_fetch_not_found( claim = crypto.sign_json_claimset(identity, "Payload") with service_error("DID Resolution failed with status code: 404"): - client.submit_claim(claim) + client.submit_claim_and_confirm(claim) def test_untrusted_server( @@ -354,7 +351,7 @@ def test_untrusted_server( claim = crypto.sign_json_claimset(identity, "Payload") with service_error("DIDResolutionError: Certificate chain is invalid"): - client.submit_claim(claim) + client.submit_claim_and_confirm(claim) def test_consistent_kid( @@ -378,7 +375,7 @@ def test_consistent_kid( assert header["kid"] == kid # Submit the claim and verify the resulting receipt. - receipt = client.submit_claim(claim).receipt + receipt = client.submit_claim_and_confirm(claim).receipt verify_receipt(claim, trust_store, receipt) # Check that the resolved DID document contains the expected assertion diff --git a/test/test_x509.py b/test/test_x509.py index 71095012..14212caf 100644 --- a/test/test_x509.py +++ b/test/test_x509.py @@ -44,7 +44,7 @@ def test_submit_claim_x5c( # Sign and submit a dummy claim using our new identity claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) - receipt = client.submit_claim(claims).receipt + receipt = client.submit_claim_and_confirm(claims).receipt # check if the header struct contains mrenclave header assert "enclave_measurement" in receipt.phdr env_platform = os.environ.get("PLATFORM") @@ -77,7 +77,7 @@ def test_invalid_certificate_chain( claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) with service_error("Certificate chain is invalid"): - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) def test_wrong_certificate( @@ -97,7 +97,7 @@ def test_wrong_certificate( claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) with service_error("Signature verification failed"): - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) def test_untrusted_ca(client: Client): @@ -109,7 +109,7 @@ def test_untrusted_ca(client: Client): claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) with service_error("Certificate chain is invalid"): - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) def test_self_signed_trusted( @@ -134,7 +134,7 @@ def test_self_signed_trusted( # We're pretty flexible about the error message here, because the exact # behaviour depends on the OpenSSL version. with service_error("Certificate chain"): - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) def test_multiple_trusted_roots(client: Client, trust_store: TrustStore): @@ -154,8 +154,8 @@ def test_multiple_trusted_roots(client: Client, trust_store: TrustStore): second_identity = second_ca.create_identity(alg="ES256", kty="ec") second_claims = crypto.sign_json_claimset(second_identity, {"foo": "bar"}) - first_receipt = client.submit_claim(first_claims).receipt - second_receipt = client.submit_claim(second_claims).receipt + first_receipt = client.submit_claim_and_confirm(first_claims).receipt + second_receipt = client.submit_claim_and_confirm(second_claims).receipt verify_receipt(first_claims, trust_store, first_receipt) verify_receipt(second_claims, trust_store, second_receipt) @@ -172,7 +172,7 @@ def test_self_signed_untrusted(client: Client): claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) with service_error("Certificate chain is invalid"): - client.submit_claim(claims) + client.submit_claim_and_confirm(claims) def test_leaf_ca( @@ -185,7 +185,7 @@ def test_leaf_ca( identity = trusted_ca.create_identity(alg="ES256", kty="ec", ca=True) claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) with service_error("Signing certificate is CA"): - client.submit_claim(claims).receipt + client.submit_claim_and_confirm(claims).receipt def test_root_ca( @@ -199,7 +199,7 @@ def test_root_ca( identity = crypto.Signer(trusted_ca.root_key_pem, x5c=[trusted_ca.root_cert_pem]) claims = crypto.sign_json_claimset(identity, {"foo": "bar"}) with service_error("Certificate chain must include at least one CA certificate"): - client.submit_claim(claims).receipt + client.submit_claim_and_confirm(claims).receipt @pytest.mark.parametrize( @@ -251,7 +251,7 @@ def test_submit_claim_notary_x509( msg.key = crypto.cose_private_key_from_pem(identity.private_key) claim = msg.encode(tag=True) - submission = client.submit_claim(claim) + submission = client.submit_claim_and_confirm(claim) verify_receipt(claim, trust_store, submission.receipt) # Embedding the receipt requires re-encoding the unprotected header. @@ -259,4 +259,4 @@ def test_submit_claim_notary_x509( # This checks whether x5chain is preserved after re-encoding by simply # submitting the claim again. claim_with_receipt = client.get_claim(submission.tx, embed_receipt=True) - client.submit_claim(claim_with_receipt) + client.submit_claim_and_confirm(claim_with_receipt)