Skip to content
Merged
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
4 changes: 2 additions & 2 deletions fiftyone_pipeline_did/examples/fodid_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ def sample_payload():
payload[FodId.FLAGS_OFFSET] = 0x00
payload[FodId.LICENSE_ID_OFFSET:FodId.LICENSE_ID_OFFSET + 4] = \
bytes([0x78, 0x56, 0x34, 0x12])
for i in range(FodId.HASH_LENGTH):
payload[FodId.HASH_OFFSET + i] = 0x20 + i
for i in range(FodId.MATCH_KEY_LENGTH):
payload[FodId.MATCH_KEY_OFFSET + i] = 0x20 + i
return bytes(payload)


Expand Down
7 changes: 7 additions & 0 deletions fiftyone_pipeline_did/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ nothing outside 51Degrees.
alias returns the same bytes and warns with `DeprecationWarning`, and the
alias will be removed in a future release, so move callers to `match_key`.

The class constants naming the match key field follow the same
vocabulary, being `FodId.MATCH_KEY_OFFSET` and `FodId.MATCH_KEY_LENGTH`.
`FodId.HASH_OFFSET` and `FodId.HASH_LENGTH` remain as deprecated aliases
holding the same values, and a class constant cannot warn when it is
read, so move callers to the new names before the aliases are removed in
a future release.

## Parsing without exceptions

An identifier arriving from outside, in a query string, a header or a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ def _payload_length_valid(fod_id: FodId) -> bool:
whose exact lengths belong to the cloud, so any longer payload is
accepted here."""
match_key_length = FodId.GUID_LENGTH if fod_id.type is IdType.RANDOM \
else FodId.HASH_LENGTH
else FodId.MATCH_KEY_LENGTH
return len(fod_id.payload) >= FodId.HEADER_LENGTH + match_key_length


Expand Down
25 changes: 19 additions & 6 deletions fiftyone_pipeline_did/src/fiftyone_pipeline_did/fod_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,17 +181,29 @@ class FodId:
#: Byte length of the License Id field.
LICENSE_ID_LENGTH = 4
#: Byte offset of the match key field within the payload.
HASH_OFFSET = 5
MATCH_KEY_OFFSET = 5
#: Byte length of the match key field (SHA-256).
HASH_LENGTH = 32
MATCH_KEY_LENGTH = 32
#: Byte length of the header (Flags + License Id) common to every type.
HEADER_LENGTH = HASH_OFFSET
HEADER_LENGTH = MATCH_KEY_OFFSET
#: Byte length of the GUID match key carried by Random identifiers.
GUID_LENGTH = 16
#: Minimum byte length of a Random 51Did payload.
RANDOM_PAYLOAD_LENGTH = HEADER_LENGTH + GUID_LENGTH
#: Minimum byte length of a Probabilistic or HashedEmail 51Did payload.
PAYLOAD_LENGTH = HASH_OFFSET + HASH_LENGTH
PAYLOAD_LENGTH = MATCH_KEY_OFFSET + MATCH_KEY_LENGTH
#: Deprecated alias for :attr:`MATCH_KEY_OFFSET`. The stable,
#: comparable part of a 51Did is now called the match key, mirroring
#: the Model Terms for Marketing vocabulary. A class constant cannot
#: warn when it is read, so this alias holds the same value and will
#: be removed in a future release.
HASH_OFFSET = MATCH_KEY_OFFSET
#: Deprecated alias for :attr:`MATCH_KEY_LENGTH`. The stable,
#: comparable part of a 51Did is now called the match key, mirroring
#: the Model Terms for Marketing vocabulary. A class constant cannot
#: warn when it is read, so this alias holds the same value and will
#: be removed in a future release.
HASH_LENGTH = MATCH_KEY_LENGTH

def __init__(self, owid: Owid) -> None:
"""Promotes an already-parsed :class:`~fiftyone_pipeline_did.Owid`
Expand Down Expand Up @@ -531,7 +543,8 @@ def _read_payload(payload: bytes) -> Tuple[FodIdParseStatus, int, int, bytes]:
# bytes is immutable, so slicing yields a match key that cannot be used
# to change the underlying payload and no defensive copy is required.
match_key = bytes(
payload[FodId.HASH_OFFSET:FodId.HASH_OFFSET + match_key_length])
payload[FodId.MATCH_KEY_OFFSET:
FodId.MATCH_KEY_OFFSET + match_key_length])
return FodIdParseStatus.PARSED, flags, license_id, match_key


Expand All @@ -550,7 +563,7 @@ def _match_key_length(id_type: IdType, payload: bytes) -> int:
return FodId.GUID_LENGTH
if id_type is IdType.RESERVED:
return len(payload) - FodId.HEADER_LENGTH
return FodId.HASH_LENGTH
return FodId.MATCH_KEY_LENGTH


def _payload_message(status: FodIdParseStatus, payload: bytes) -> str:
Expand Down
6 changes: 3 additions & 3 deletions fiftyone_pipeline_did/tests/envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def probabilistic_payload():
payload[FodId.FLAGS_OFFSET] = 0b0000_0101
payload[FodId.LICENSE_ID_OFFSET:FodId.LICENSE_ID_OFFSET + 4] = \
bytes([0x78, 0x56, 0x34, 0x12])
for i in range(FodId.HASH_LENGTH):
payload[FodId.HASH_OFFSET + i] = 0x20 + i
for i in range(FodId.MATCH_KEY_LENGTH):
payload[FodId.MATCH_KEY_OFFSET + i] = 0x20 + i
return bytes(payload)


Expand All @@ -57,7 +57,7 @@ def random_payload():
payload = bytearray(FodId.RANDOM_PAYLOAD_LENGTH)
payload[FodId.FLAGS_OFFSET] = (1 << 6) | 0b001
for i in range(FodId.GUID_LENGTH):
payload[FodId.HASH_OFFSET + i] = 0x40 + i
payload[FodId.MATCH_KEY_OFFSET + i] = 0x40 + i
return bytes(payload)


Expand Down
8 changes: 4 additions & 4 deletions fiftyone_pipeline_did/tests/test_did_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ def setUp(self):
# encoded: every three bytes of 0xFB encode to "+/v7" whatever
# the alignment, and a 32 byte run holds several whole triples.
payload = bytearray(probabilistic_payload())
for i in range(FodId.HASH_LENGTH):
payload[FodId.HASH_OFFSET + i] = 0xFB
for i in range(FodId.MATCH_KEY_LENGTH):
payload[FodId.MATCH_KEY_OFFSET + i] = 0xFB
self.fod_id = signed_fod_id(self.crypto, bytes(payload))
self.standard = self.fod_id.as_base64()
self.assertTrue("+" in self.standard or "/" in self.standard,
Expand Down Expand Up @@ -491,8 +491,8 @@ def test_string_form_is_sent_as_given_and_encoded(self):
# A payload of 0xFB bytes encodes to "+/v7" whatever the alignment,
# so the standard form carries both characters that need encoding.
payload = bytearray(probabilistic_payload())
for i in range(FodId.HASH_LENGTH):
payload[FodId.HASH_OFFSET + i] = 0xFB
for i in range(FodId.MATCH_KEY_LENGTH):
payload[FodId.MATCH_KEY_OFFSET + i] = 0xFB
standard = signed_fod_id(Crypto.new(), bytes(payload)).as_base64()
self.assertIn("+", standard)
self.assertIn("/", standard)
Expand Down
30 changes: 19 additions & 11 deletions fiftyone_pipeline_did/tests/test_fodid.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
# 0xA5: usage bits plus the HashedEmail type tag in bits 6-7.
CANONICAL_FLAGS = 0xA5
CANONICAL_LICENSE_ID = 0x12345678
CANONICAL_MATCH_KEY = bytes((0x20 + i) for i in range(FodId.HASH_LENGTH))
CANONICAL_MATCH_KEY = bytes((0x20 + i) for i in range(FodId.MATCH_KEY_LENGTH))

#: A creator domain longer than the one the cloud signs with, as a
#: self-hosted container may be configured to use.
Expand All @@ -61,7 +61,8 @@ def canonical_payload():
payload = bytearray(FodId.PAYLOAD_LENGTH)
payload[FodId.FLAGS_OFFSET] = CANONICAL_FLAGS
_write_license_id(payload)
payload[FodId.HASH_OFFSET:FodId.HASH_OFFSET + FodId.HASH_LENGTH] = \
payload[FodId.MATCH_KEY_OFFSET:
FodId.MATCH_KEY_OFFSET + FodId.MATCH_KEY_LENGTH] = \
CANONICAL_MATCH_KEY
return bytearray(payload)

Expand All @@ -71,7 +72,7 @@ def canonical_random_payload():
payload[FodId.FLAGS_OFFSET] = (1 << 6) | 0b001 # Random tag + usage bits
_write_license_id(payload)
for i in range(FodId.GUID_LENGTH):
payload[FodId.HASH_OFFSET + i] = 0x40 + i
payload[FodId.MATCH_KEY_OFFSET + i] = 0x40 + i
return bytearray(payload)


Expand Down Expand Up @@ -104,13 +105,20 @@ def setUp(self):
# ----- Current .NET coverage -----

def test_constants_are_internally_consistent(self):
self.assertEqual(FodId.HASH_OFFSET + FodId.HASH_LENGTH,
self.assertEqual(FodId.MATCH_KEY_OFFSET + FodId.MATCH_KEY_LENGTH,
FodId.PAYLOAD_LENGTH)
self.assertEqual(FodId.LICENSE_ID_OFFSET + FodId.LICENSE_ID_LENGTH,
FodId.HASH_OFFSET)
self.assertEqual(FodId.HASH_OFFSET + FodId.GUID_LENGTH,
FodId.MATCH_KEY_OFFSET)
self.assertEqual(FodId.MATCH_KEY_OFFSET + FodId.GUID_LENGTH,
FodId.RANDOM_PAYLOAD_LENGTH)

def test_deprecated_constant_aliases_hold_the_new_values(self):
# The offset and length of the match key field are now named after
# the match key. The old names stay for a release as aliases
# holding the same values.
self.assertEqual(FodId.MATCH_KEY_OFFSET, FodId.HASH_OFFSET)
self.assertEqual(FodId.MATCH_KEY_LENGTH, FodId.HASH_LENGTH)

def test_exposes_owid_level_fields(self):
fod = FodId.from_base64(
self.factory.signed_owid_base64(canonical_payload()))
Expand Down Expand Up @@ -235,7 +243,7 @@ def test_payload_larger_than_spec_uses_first_37_bytes(self):
self.assertEqual(CANONICAL_FLAGS, fod.flags)
self.assertEqual(CANONICAL_LICENSE_ID, fod.license_id)
self.assertEqual(CANONICAL_MATCH_KEY, fod.match_key)
self.assertEqual(FodId.HASH_LENGTH, len(fod.match_key))
self.assertEqual(FodId.MATCH_KEY_LENGTH, len(fod.match_key))

def test_long_envelope_parses_and_keeps_the_header_fields(self):
# No upper bound belongs in the reader: a creator domain is a
Expand All @@ -250,7 +258,7 @@ def test_long_envelope_parses_and_keeps_the_header_fields(self):
self.assertEqual(CANONICAL_FLAGS, fod.flags)
self.assertEqual(CANONICAL_LICENSE_ID, fod.license_id)
self.assertEqual(CANONICAL_MATCH_KEY, fod.match_key)
self.assertEqual(FodId.HASH_LENGTH, len(fod.match_key))
self.assertEqual(FodId.MATCH_KEY_LENGTH, len(fod.match_key))

def test_is_cryptographically_verifiable(self):
fod = FodId.from_base64(
Expand Down Expand Up @@ -316,7 +324,7 @@ def test_hashed_email_payload_one_byte_short_raises(self):
FodId.from_base64(base64)

def test_reserved_header_only_parses(self):
payload = bytearray(FodId.HASH_OFFSET)
payload = bytearray(FodId.MATCH_KEY_OFFSET)
payload[FodId.FLAGS_OFFSET] = 0b1100_0000
fod = FodId.from_base64(self.factory.signed_owid_base64(payload))
self.assertEqual(IdType.RESERVED, fod.type)
Expand Down Expand Up @@ -367,7 +375,7 @@ def test_source_envelope_cannot_be_changed_after_construction(self):
with self.assertRaises(AttributeError):
owid.signature = bytes(64)
self.assertEqual(CANONICAL_MATCH_KEY, fod.match_key)
self.assertEqual(0x20, fod.payload[FodId.HASH_OFFSET])
self.assertEqual(0x20, fod.payload[FodId.MATCH_KEY_OFFSET])

def test_constructor_reads_the_envelope_back_through_the_parser(self):
# The envelope handed in is written out and read back, so the FodId
Expand Down Expand Up @@ -487,7 +495,7 @@ def test_longer_creator_context_section_is_accepted(self):
fod = self.assert_parsed(FodId.try_from_base64(
self.factory.signed_owid_base64(payload)))
self.assert_canonical(fod)
self.assertEqual(FodId.HASH_LENGTH, len(fod.match_key))
self.assertEqual(FodId.MATCH_KEY_LENGTH, len(fod.match_key))
self.assertEqual(payload, fod.payload)

def test_far_longer_payload_is_not_rejected_for_its_length(self):
Expand Down
Loading