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
16 changes: 9 additions & 7 deletions fiftyone_pipeline_did/examples/fodid_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
ECDSA P-256 key pair, sign a canonical 37-byte payload - then parses it back
and prints the three payload fields. It also shows the headline use case: a
51Did is re-issued fresh on every call (the envelope, hence the base64,
changes), but the value (the Hash) is stable. Compare values, never envelopes.
changes), but the match key is stable. Compare match keys, never
envelopes.
"""

from owid import Crypto, Creator
Expand All @@ -39,7 +40,7 @@

def sample_payload():
"""A canonical 37-byte Probabilistic payload: flags 0x00, License Id
0x12345678 (little-endian) and a 32-byte value 0x20..0x3F."""
0x12345678 (little-endian) and a 32-byte match key 0x20..0x3F."""
payload = bytearray(FodId.PAYLOAD_LENGTH)
payload[FodId.FLAGS_OFFSET] = 0x00
payload[FodId.LICENSE_ID_OFFSET:FodId.LICENSE_ID_OFFSET + 4] = \
Expand Down Expand Up @@ -70,21 +71,22 @@ def run():
print(" Type :", fod_id.type.name)
print(" Flags : 0x{:02x}".format(fod_id.flags))
print(" LicenseId :", fod_id.license_id)
print(" Hash :", fod_id.hash.hex())
print(" Match key :", fod_id.match_key.hex())
print(" Verifies :", fod_id.verify(crypto.public_key_pem()))

reissued = FodId.from_base64(issue(creator, payload))
same_envelope = fod_id.as_base64() == reissued.as_base64()
same_value = fod_id.hash == reissued.hash
same_match_key = fod_id.match_key == reissued.match_key

print()
print("Same payload, re-issued:")
print(" Same envelope (base64) :", same_envelope)
print(" Same value (Hash) :", same_value)
print(" Same match key :", same_match_key)

if same_envelope or not same_value:
if same_envelope or not same_match_key:
raise AssertionError(
"Expected a different envelope but the same value across reissues.")
"Expected a different envelope but the same match key across "
"reissues.")


if __name__ == "__main__":
Expand Down
44 changes: 25 additions & 19 deletions fiftyone_pipeline_did/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,24 @@ Identifier) returned by the 51Degrees Cloud service. Mirrors the .NET
- The **envelope** is the data model that carries it: a signed OWID holding
the version, domain, date, payload and signature. It changes byte-for-byte
every time the cloud issues one.
- The **value** is the stable, comparable part of the payload after the Flags
and License Id: a 32-byte SHA-256 for Probabilistic and HashedEmail
identifiers, or 16 GUID bytes for Random.
- The **match key** is the stable, comparable part of the payload after
the Flags and License Id, being a 32-byte SHA-256 for Probabilistic and
HashedEmail identifiers, or 16 GUID bytes for Random. Two 51Dids for the
same inputs share the same match key even though their envelopes differ.

**Comparing two 51Dids means comparing their values, never their envelopes.**
**Comparing two 51Dids means comparing their match keys, never their
envelopes.**

## Payload layout

| Offset | Length | Field | Type |
|-------:|-------:|------------|-------------------------------------------------|
| 0 | 1 | Flags | uint8: bits 0-2 usage, bits 6-7 identifier type |
| 1 | 4 | LicenseId | uint32 (little-endian) |
| 5 | 16/32 | Value | SHA-256 (Probabilistic, HashedEmail) or GUID (Random) |
| 5 | 16/32 | Match key | SHA-256 (Probabilistic, HashedEmail) or GUID (Random) |

| Bits 7-6 | `IdType` | Value length | Minimum payload |
|---------:|-----------------|-------------:|----------------:|
| Bits 7-6 | `IdType` | Match key length | Minimum payload |
|---------:|-----------------|-----------------:|----------------:|
| `00` | `PROBABILISTIC` | 32 | 37 |
| `01` | `RANDOM` | 16 | 21 |
| `10` | `HASHED_EMAIL` | 32 | 37 |
Expand Down Expand Up @@ -81,7 +83,7 @@ fod_id = FodId.from_base64(base64_from_cloud_service) # either alphabet
flags = fod_id.flags
type_ = fod_id.type # IdType.PROBABILISTIC / RANDOM / HASHED_EMAIL
license_id = fod_id.license_id
value = fod_id.hash # SHA-256 or GUID bytes, see type
match_key = fod_id.match_key # SHA-256 or GUID bytes, see type

# Delegated OWID-level fields and operations.
domain = fod_id.domain
Expand All @@ -98,6 +100,10 @@ encrypted value that only 51Degrees can turn back into a licence
identifier, so `license_id` is the field's raw value and identifies
nothing outside 51Degrees.

`fod_id.hash` remains as a deprecated alias of `match_key`. Reading the
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`.

## Parsing without exceptions

An identifier arriving from outside, in a query string, a header or a
Expand Down Expand Up @@ -158,21 +164,21 @@ same whichever language parsed the bytes.
| `ABSENT_NODE` | The version 0 marker, which stands for an absent envelope |
| `MALFORMED_ENVELOPE` | Malformed in a way none of the above describes |
| `PAYLOAD_TOO_SHORT` | The envelope was read but the payload is shorter than the 5 byte header, so the type cannot be read |
| `INVALID_TYPE_PAYLOAD_LENGTH` | The header names a type whose value needs more bytes than the payload holds |
| `INVALID_TYPE_PAYLOAD_LENGTH` | The header names a type whose match key needs more bytes than the payload holds |

### Lower bounds and no upper bound

The payload must hold the 5 byte header before the type can be read, and
the type then says how many value bytes must follow, being 16 for
the type then says how many match key bytes must follow, being 16 for
`RANDOM` and 32 for `PROBABILISTIC` and `HASHED_EMAIL`, as the payload
layout table above shows. `RESERVED` keeps the best-effort reading, being
the header fields and whatever bytes follow. Anything beyond the value is
a creator context section whose lengths belong to the cloud, so a longer
payload, a longer creator domain (a self-hosted container may sign with
one) or a longer envelope is accepted and this package places no upper
bound of its own on any of them. An older reader meeting a context
the header fields and whatever bytes follow. Anything beyond the match key
is a creator context section whose lengths belong to the cloud, so a
longer payload, a longer creator domain (a self-hosted container may sign
with one) or a longer envelope is accepted and this package places no
upper bound of its own on any of them. An older reader meeting a context
section of a version it does not know still reads the header and the
value.
match key.

`DidClient` refuses text longer than 4096 characters before it parses
it, fetches a key or calls the cloud. That figure is client policy,
Expand Down Expand Up @@ -234,8 +240,8 @@ a = FodId.from_base64(idprobglobal_a)
b = FodId.from_base64(idprobglobal_b)

# The envelope (date, signature, base64) differs across reissues.
# The value inside the payload is stable - this is what you compare:
same_value = a.hash == b.hash
# The match key inside the payload is stable, so compare match keys:
same_match_key = a.match_key == b.match_key
```

## Verifying on your server
Expand Down Expand Up @@ -491,7 +497,7 @@ is refreshed by common-ci's `update-example-assets` step.
be genuine. Call `verify(public_key_pem)`, `signature_status(public_key_pem)`
or a `DidClient` check when needed.
- **No upper bound on the size of an identifier.** The lengths beyond the
header and value belong to the cloud. The 4096 character figure in
header and match key belong to the cloud. The 4096 character figure in
`DidClient` is client policy against obviously malformed text, not a
format limit.
- **No creation of new 51Dids.** This is a parser; new 51Dids are issued by the
Expand Down
2 changes: 1 addition & 1 deletion fiftyone_pipeline_did/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def read(file_name):
author="51Degrees Engineering",
author_email="engineering@51degrees.com",
url="https://51degrees.com/?utm_source=pypi&utm_medium=package&utm_campaign=pipeline-python&utm_content=fiftyone_pipeline_did-setup.py&utm_term=url",
description=("Strongly typed reader and cloud client for the 51Did (51Degrees Identifier) value returned by the 51Degrees Cloud service. Parses the OWID envelope in either base64 alphabet and exposes the Flags, License Id and value (Hash) plus the identifier type, and verifies a 51Did's signature offline or through the cloud and redeems a sealed creator context result on the server. Compare values, never envelopes."),
description=("Strongly typed reader and cloud client for the 51Did (51Degrees Identifier) value returned by the 51Degrees Cloud service. Parses the OWID envelope in either base64 alphabet and exposes the Flags, License Id and match key plus the identifier type, and verifies a 51Did's signature offline or through the cloud and redeems a sealed creator context result on the server. Compare match keys, never envelopes."),
long_description=read("readme.md"),
long_description_content_type='text/markdown',
python_requires=">=3.9",
Expand Down
6 changes: 3 additions & 3 deletions fiftyone_pipeline_did/src/fiftyone_pipeline_did/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@

:class:`~fiftyone_pipeline_did.fod_id.FodId` parses a 51Did from its base64
OWID form in either alphabet, exposes the three payload fields (Flags,
License Id and the value Hash) and the identifier
License Id and the match key) and the identifier
:class:`~fiftyone_pipeline_did.id_type.IdType`, and delegates OWID-level
concerns to the wrapped envelope. ``FodId.try_from_base64`` and
``FodId.try_from_byte_array`` read external data without raising and answer
with a :class:`~fiftyone_pipeline_did.fod_id.FodIdParseResult` naming the
:class:`~fiftyone_pipeline_did.fod_id.FodIdParseStatus` either way. Parsing
never checks the signature. Compare 51Dids by their value (``hash``), never
by their envelopes.
never checks the signature. Compare 51Dids by their match key
(``match_key``), never by their envelopes.

:class:`~fiftyone_pipeline_did.did_client.DidClient` handles every
manipulation of a 51Did a server needs against the 51Degrees cloud: the
Expand Down
4 changes: 2 additions & 2 deletions fiftyone_pipeline_did/src/fiftyone_pipeline_did/did_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,9 +763,9 @@ def _payload_length_valid(fod_id: FodId) -> bool:
identifier. Anything beyond the base is a creator context section,
whose exact lengths belong to the cloud, so any longer payload is
accepted here."""
value_length = FodId.GUID_LENGTH if fod_id.type is IdType.RANDOM \
match_key_length = FodId.GUID_LENGTH if fod_id.type is IdType.RANDOM \
else FodId.HASH_LENGTH
return len(fod_id.payload) >= FodId.HEADER_LENGTH + value_length
return len(fod_id.payload) >= FodId.HEADER_LENGTH + match_key_length


def _in_force_at(keys: List[PublicKeyEntry],
Expand Down
Loading
Loading