Skip to content

Commit

Permalink
Cose receipts verify (#6603)
Browse files Browse the repository at this point in the history
  • Loading branch information
achamayou authored Oct 31, 2024
1 parent 35560e9 commit 79ffcdb
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 1 deletion.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [6.0.0-dev4]

[6.0.0-dev4]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev4

### Added

- `ccf.cose.verify_receipt()` to support verifiying [draft COSE receipts](https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/) (#6603).

### Removed

- Remove SECP256K1 support as a part of the migration to Azure Linux (#6592).
Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "ccf"
version = "6.0.0-dev3"
version = "6.0.0-dev4"
authors = [
{ name="CCF Team", email="[email protected]" },
]
Expand Down
54 changes: 54 additions & 0 deletions python/src/ccf/cose.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import base64
import cbor2
import json
from hashlib import sha256
from datetime import datetime
import pycose.headers # type: ignore
from pycose.keys.ec2 import EC2Key # type: ignore
Expand All @@ -24,6 +25,8 @@
from cryptography.x509 import load_pem_x509_certificate
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509.base import CertificatePublicKeyTypes
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

Pem = str

Expand All @@ -37,6 +40,17 @@
"encrypted_recovery_share",
] + GOV_MSG_TYPES_WITH_PROPOSAL_ID

# See https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/
# should move to a pycose.header value after RFC publication

COSE_PHDR_VDP_LABEL = 396
COSE_RECEIPT_INCLUSION_PROOF_LABEL = -1

# See https://datatracker.ietf.org/doc/draft-birkholz-cose-receipts-ccf-profile/

CCF_PROOF_LEAF_LABEL = 1
CCF_PROOF_PATH_LABEL = 2


def from_cryptography_eckey_obj(ext_key) -> EC2Key:
"""
Expand Down Expand Up @@ -187,6 +201,46 @@ def validate_cose_sign1(pubkey, cose_sign1, payload=None):
raise ValueError("signature is invalid")


def verify_receipt(receipt_bytes: bytes, key: CertificatePublicKeyTypes):
"""
Verify a COSE Sign1 receipt as defined in https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/,
using the CCF tree algorithm defined in https://datatracker.ietf.org/doc/draft-birkholz-cose-receipts-ccf-profile/
"""
# Extract the expected KID from the public key used for verification,
# and check it against the value set in the COSE header before using
# it to verify the proofs.
expected_kid = sha256(
key.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
).digest()
receipt = Sign1Message.decode(receipt_bytes)
cose_key = from_cryptography_eckey_obj(key)
assert receipt.phdr[pycose.headers.KID] == expected_kid
receipt.key = cose_key

assert COSE_PHDR_VDP_LABEL in receipt.uhdr, "Verifiable data proof is required"
proof = receipt.uhdr[COSE_PHDR_VDP_LABEL]
assert COSE_RECEIPT_INCLUSION_PROOF_LABEL in proof, "Inclusion proof is required"
inclusion_proofs = proof[COSE_RECEIPT_INCLUSION_PROOF_LABEL]
assert inclusion_proofs, "At least one inclusion proof is required"
for inclusion_proof in inclusion_proofs:
assert isinstance(inclusion_proof, bytes), "Inclusion proof must be bstr"
proof = cbor2.loads(inclusion_proof)
assert CCF_PROOF_LEAF_LABEL in proof, "Leaf must be present"
leaf = proof[CCF_PROOF_LEAF_LABEL]
accumulator = sha256(
leaf[0] + sha256(leaf[1].encode()).digest() + leaf[2]
).digest()
assert CCF_PROOF_PATH_LABEL in proof, "Path must be present"
path = proof[CCF_PROOF_PATH_LABEL]
for left, digest in path:
if left:
accumulator = sha256(digest + accumulator).digest()
else:
accumulator = sha256(accumulator + digest).digest()
if not receipt.verify_signature(accumulator):
raise ValueError("Signature verification failed")


_SIGN_DESCRIPTION = """Create and sign a COSE Sign1 message for CCF governance
Note that this tool writes binary COSE Sign1 to standard output.
Expand Down
7 changes: 7 additions & 0 deletions tests/e2e_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,12 @@ def test_cose_signature_schema(network, args):
def test_cose_receipt_schema(network, args):
primary, _ = network.find_nodes()

service_cert_path = os.path.join(network.common_dir, "service_cert.pem")
service_cert = load_pem_x509_certificate(
open(service_cert_path, "rb").read(), default_backend()
)
service_key = service_cert.public_key()

with primary.client("user0") as client:
r = client.get("/commit")
assert r.status_code == http.HTTPStatus.OK
Expand All @@ -1033,6 +1039,7 @@ def test_cose_receipt_schema(network, args):
)
if r.status_code == http.HTTPStatus.OK:
cbor_proof = r.body.data()
ccf.cose.verify_receipt(cbor_proof, service_key)
cbor_proof_filename = os.path.join(
network.common_dir, f"receipt_{txid}.cose"
)
Expand Down

0 comments on commit 79ffcdb

Please sign in to comment.