From d456cf2630495d778f3c930fa4092bd05a2f678e Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 31 Aug 2026 16:17:15 +0100 Subject: [PATCH 1/3] Rename FodId.getHash to getMatchKey to match the Model Terms vocabulary The stable, comparable part of a 51Did, the payload bytes after the Flags and License Id, is called the match key in the Model Terms for Marketing and in the patent, so the reader now uses the same word. getHash() stays as a deprecated alias forwarding to getMatchKey() so that existing callers keep compiling and get the same bytes. The private field, constructor parameter and parse walk locals move to match key naming. HASH_OFFSET and HASH_LENGTH keep their names because the SHA-256 wording behind them stays true, and their javadoc now describes the match key field. The class javadoc, package-info, pom description, FodIdParseStatus javadoc and README use the new word. --- pipeline.did/README.md | 29 +++--- pipeline.did/pom.xml | 4 +- .../java/fiftyone/pipeline/did/FodId.java | 98 +++++++++++-------- .../pipeline/did/FodIdParseStatus.java | 4 +- .../fiftyone/pipeline/did/package-info.java | 8 +- 5 files changed, 83 insertions(+), 60 deletions(-) diff --git a/pipeline.did/README.md b/pipeline.did/README.md index be047f66a..21b9f18f1 100644 --- a/pipeline.did/README.md +++ b/pipeline.did/README.md @@ -13,26 +13,27 @@ A 51Did is described at three levels, and the wording is deliberate. the version, domain, date, payload and signature. It changes byte-for-byte every time the cloud issues one, even for the same inputs, because the date and signature change on each call. -- 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 +- The **match key** 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. Two 51Dids for the same inputs - share the same value even though their envelopes differ. + 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 -The header is shared by every identifier type; bits 6-7 of Flags select the -type and the length of the value that follows. +The header is shared by every identifier type. Bits 6-7 of Flags select the +type and the length of the match key that follows. | 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) | | after | any | Context | Optional creator context section, readable only by 51Degrees | -| 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 | @@ -172,7 +173,7 @@ FodId fodId = FodId.fromBase64(base64FromCloudService); int flags = fodId.getFlags(); IdType type = fodId.getType(); // PROBABILISTIC / RANDOM / HASHED_EMAIL long licenseId = fodId.getLicenseId(); -byte[] hash = fodId.getHash(); // SHA-256 or GUID bytes, see type +byte[] matchKey = fodId.getMatchKey(); // SHA-256 or GUID bytes, see type // Delegated OWID-level fields and operations. String domain = fodId.getDomain(); @@ -189,11 +190,13 @@ FodId a = FodId.fromBase64(idprobglobalA); FodId b = FodId.fromBase64(idprobglobalB); // The envelope (date, signature, base64) differs across reissues. -// The value inside the payload is stable - this is what you compare: -boolean sameValue = java.util.Arrays.equals(a.getHash(), b.getHash()); +// The match key inside the payload is stable - this is what you compare: +boolean sameMatchKey = java.util.Arrays.equals(a.getMatchKey(), b.getMatchKey()); ``` -Use `getHash()` as the cache / dedup key. +Use `getMatchKey()` as the cache / dedup key. `getHash()` remains as a +deprecated alias of `getMatchKey()`, returning the same bytes, and will be +removed in a future release. ## Verifying on your server @@ -342,4 +345,4 @@ signed envelopes there is no longer a copy made inside it. - **No creation of new 51Dids.** This is a reader and a verifier; new 51Dids are issued by the 51Degrees cloud / on-premise hashing engines. - **No upper bound on a payload.** The cloud owns the shape of anything past - the value, and this package does not second-guess it. + the match key, and this package does not second-guess it. diff --git a/pipeline.did/pom.xml b/pipeline.did/pom.xml index 76f140c31..4598505c4 100644 --- a/pipeline.did/pom.xml +++ b/pipeline.did/pom.xml @@ -37,8 +37,8 @@ 51Degrees :: Pipeline :: 51Did Strongly typed reader for the 51Did (51Degrees Identifier) value returned by the 51Degrees Cloud service. Parses the OWID - envelope and exposes the Flags, License Id and value (Hash) plus the - identifier type. Compare values, never envelopes. + envelope and exposes the Flags, License Id and match key plus the + identifier type. Compare match keys, never envelopes. https://51degrees.com?utm_source=maven&utm_medium=package&utm_campaign=pipeline-java&utm_content=pipeline.did-pom.xml&utm_term=url diff --git a/pipeline.did/src/main/java/fiftyone/pipeline/did/FodId.java b/pipeline.did/src/main/java/fiftyone/pipeline/did/FodId.java index 8fd6e23ca..5445809be 100644 --- a/pipeline.did/src/main/java/fiftyone/pipeline/did/FodId.java +++ b/pipeline.did/src/main/java/fiftyone/pipeline/did/FodId.java @@ -42,25 +42,26 @@ * A 51Did is described at three levels, and the wording here is deliberate. * The 51Did is the identifier as a whole. The envelope is the * signed {@link Owid} that carries it (version, domain, date, payload, - * signature), re-issued fresh on every call. The value is the stable, - * comparable part of the payload after the Flags and License Id, exposed as - * {@link #getHash()}. Two 51Dids for the same inputs share the same value even - * though their envelopes differ on every issue. Compare values, never - * envelopes. + * signature), re-issued fresh on every call. The match key is the + * stable, comparable part of the payload after the Flags and License Id, + * exposed as {@link #getMatchKey()}. Two 51Dids for the same inputs share the + * same match key even though their envelopes differ on every issue. + * Compare match keys, never envelopes. *

- * Payload layout. The header (offsets 0-4) is shared by every identifier type; - * bits 6-7 of Flags select the {@link IdType} and the length of the value that - * follows: + * Payload layout. The header (offsets 0-4) is shared by every identifier type. + * Bits 6-7 of Flags select the {@link IdType} and the length of the match key + * that follows: *

*

* Reading and verifying are two separate steps. {@link #tryFromBase64(String)} @@ -95,10 +96,10 @@ public final class FodId { /** Byte length of the License Id field. */ public static final int LICENSE_ID_LENGTH = 4; - /** Byte offset of the value (Hash) field within the payload. */ + /** Byte offset of the match key field within the payload. */ public static final int HASH_OFFSET = 5; - /** Byte length of the SHA-256 value. */ + /** Byte length of the match key field (SHA-256). */ public static final int HASH_LENGTH = 32; /** @@ -107,7 +108,7 @@ public final class FodId { */ public static final int HEADER_LENGTH = HASH_OFFSET; - /** Byte length of the GUID value carried by Random identifiers. */ + /** Byte length of the GUID match key carried by Random identifiers. */ public static final int GUID_LENGTH = 16; /** @@ -118,7 +119,7 @@ public final class FodId { /** * Minimum byte length of a Probabilistic or HashedEmail 51Did payload - * (Flags + License Id + Hash). Random payloads are shorter - see + * (Flags + License Id + match key). Random payloads are shorter - see * {@link #RANDOM_PAYLOAD_LENGTH}. */ public static final int PAYLOAD_LENGTH = HASH_OFFSET + HASH_LENGTH; @@ -132,18 +133,18 @@ public final class FodId { private final Owid owid; private final int flags; private final long licenseId; - private final byte[] hash; + private final byte[] matchKey; /** * Built only by {@link #read(Owid)} once the payload has passed the * 51Did rules, so an instance never exists for a payload that failed * them. */ - private FodId(Owid owid, int flags, long licenseId, byte[] hash) { + private FodId(Owid owid, int flags, long licenseId, byte[] matchKey) { this.owid = owid; this.flags = flags; this.licenseId = licenseId; - this.hash = hash; + this.matchKey = matchKey; } // ----- Reading without throwing ----- @@ -205,7 +206,8 @@ private static FodIdParseResult read(OwidParseResult envelope) { * The rules are lower bounds only. The header must be present before the * type can be read, and the type then sets the least the payload can * hold. Anything longer is accepted as it stands, because the bytes past - * the value are a creator context section whose shape the cloud judges. + * the match key are a creator context section whose shape the cloud + * judges. */ private static FodIdParseResult read(Owid owid) { byte[] payload = owid.getPayload(); @@ -213,21 +215,22 @@ private static FodIdParseResult read(Owid owid) { return FodIdParseResult.failed(FodIdParseStatus.PAYLOAD_TOO_SHORT); } int flags = payload[FLAGS_OFFSET] & 0xFF; - int valueLength; + int matchKeyLength; switch (IdType.fromFlags(flags)) { case RANDOM: - valueLength = GUID_LENGTH; + matchKeyLength = GUID_LENGTH; break; case RESERVED: // Not yet assigned, so read best-effort. The header fields - // are unpacked and whatever follows is exposed as the value. - valueLength = payload.length - HEADER_LENGTH; + // are unpacked and whatever follows is exposed as the + // match key. + matchKeyLength = payload.length - HEADER_LENGTH; break; default: - valueLength = HASH_LENGTH; + matchKeyLength = HASH_LENGTH; break; } - if (payload.length < HEADER_LENGTH + valueLength) { + if (payload.length < HEADER_LENGTH + matchKeyLength) { return FodIdParseResult.failed( FodIdParseStatus.INVALID_TYPE_PAYLOAD_LENGTH); } @@ -238,12 +241,13 @@ private static FodIdParseResult read(Owid owid) { | ((payload[LICENSE_ID_OFFSET + 1] & 0xFFL) << 8) | ((payload[LICENSE_ID_OFFSET + 2] & 0xFFL) << 16) | ((payload[LICENSE_ID_OFFSET + 3] & 0xFFL) << 24); - // The value is copied out so that mutating the array a caller gets - // back from getHash() can never reach the envelope's own bytes. - byte[] hash = Arrays.copyOfRange( - payload, HASH_OFFSET, HASH_OFFSET + valueLength); + // The match key is copied out so that mutating the array a caller + // gets back from getMatchKey() can never reach the envelope's own + // bytes. + byte[] matchKey = Arrays.copyOfRange( + payload, HASH_OFFSET, HASH_OFFSET + matchKeyLength); return FodIdParseResult.parsed( - new FodId(owid, flags, licenseId, hash)); + new FodId(owid, flags, licenseId, matchKey)); } // ----- Reading with exceptions ----- @@ -396,15 +400,31 @@ public long getLicenseId() { } /** - * Returns the value bytes from the payload (a 32-byte SHA-256 for + * Returns the match key from the payload (a 32-byte SHA-256 for * Probabilistic and HashedEmail identifiers, or 16 GUID bytes for Random). - * This is the stable, comparable part of the envelope - use it as the - * cache / dedup key. + * The match key is the stable, comparable part of the envelope. Two + * 51Dids for the same inputs share the same match key even though their + * envelopes (date, signature) differ on every issue, so use the match + * key as the cache / dedup key. * - * @return a defensive copy of the value bytes + * @return a defensive copy of the match key bytes */ + public byte[] getMatchKey() { + return matchKey.clone(); + } + + /** + * Deprecated alias for {@link #getMatchKey()}. The stable, comparable + * part of a 51Did is now called the match key, mirroring the Model Terms + * for Marketing vocabulary. This alias will be removed in a future + * release. + * + * @return the same bytes as {@link #getMatchKey()} + * @deprecated renamed to {@link #getMatchKey()} + */ + @Deprecated public byte[] getHash() { - return hash.clone(); + return getMatchKey(); } /** @return the OWID version. */ diff --git a/pipeline.did/src/main/java/fiftyone/pipeline/did/FodIdParseStatus.java b/pipeline.did/src/main/java/fiftyone/pipeline/did/FodIdParseStatus.java index a9f998561..447f1c07a 100644 --- a/pipeline.did/src/main/java/fiftyone/pipeline/did/FodIdParseStatus.java +++ b/pipeline.did/src/main/java/fiftyone/pipeline/did/FodIdParseStatus.java @@ -110,8 +110,8 @@ public enum FodIdParseStatus { * The envelope read and the header names a type, but the payload is * shorter than the minimum for that type. Random needs the header plus * 16 GUID bytes, and Probabilistic and HashedEmail need the header plus - * a 32 byte hash. A longer payload is never refused here, because - * anything past the value is a creator context section whose lengths + * a 32 byte match key. A longer payload is never refused here, because + * anything past the match key is a creator context section whose lengths * belong to the cloud. */ INVALID_TYPE_PAYLOAD_LENGTH; diff --git a/pipeline.did/src/main/java/fiftyone/pipeline/did/package-info.java b/pipeline.did/src/main/java/fiftyone/pipeline/did/package-info.java index 74d564420..fbe521ffa 100644 --- a/pipeline.did/src/main/java/fiftyone/pipeline/did/package-info.java +++ b/pipeline.did/src/main/java/fiftyone/pipeline/did/package-info.java @@ -30,11 +30,11 @@ * {@link fiftyone.pipeline.did.FodIdParseResult} instead of throwing, whose * {@link fiftyone.pipeline.did.FodIdParseStatus} names why an input is not * a 51Did, and the {@code from} readers make the same read and throw. A - * 51Did exposes the three payload fields (Flags, License Id and the value - * Hash) and the identifier {@link fiftyone.pipeline.did.IdType}, and + * 51Did exposes the three payload fields (Flags, License Id and the match + * key) and the identifier {@link fiftyone.pipeline.did.IdType}, and * delegates OWID-level concerns to the envelope it holds. Reading never - * checks the signature. Compare 51Dids by their value ({@code getHash()}), - * never by their envelopes. + * checks the signature. Compare 51Dids by their match key + * ({@code getMatchKey()}), never by their envelopes. *

* {@link fiftyone.pipeline.did.DidClient} is what a server uses against the * 51Degrees cloud: it fetches and holds the published signing keys, verifies From 3828a96161a3559e3cd738f1ffa92a58cb1d90f6 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 31 Aug 2026 16:17:16 +0100 Subject: [PATCH 2/3] Move the 51Did tests to getMatchKey and cover the deprecated alias Every test that meant the match key now calls getMatchKey(), and the constant, factory method and test names that meant it are renamed. One new test, getHash_DeprecatedAlias_ReturnsMatchKey, is the only caller of the deprecated getHash() and asserts it returns the same bytes as getMatchKey(). The root pom compiles with -Werror, so that test carries a deprecation suppression and any other use of the old name would fail the build. --- .../pipeline/did/FodIdParseTests.java | 18 ++--- .../pipeline/did/FodIdTestFactory.java | 15 +++-- .../fiftyone/pipeline/did/FodIdTests.java | 66 +++++++++++-------- 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdParseTests.java b/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdParseTests.java index 61b3f758f..9335549bc 100644 --- a/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdParseTests.java +++ b/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdParseTests.java @@ -34,7 +34,7 @@ import java.util.Arrays; import static fiftyone.pipeline.did.FodIdTestFactory.CANONICAL_FLAGS; -import static fiftyone.pipeline.did.FodIdTestFactory.CANONICAL_HASH; +import static fiftyone.pipeline.did.FodIdTestFactory.CANONICAL_MATCH_KEY; import static fiftyone.pipeline.did.FodIdTestFactory.CANONICAL_LICENSE_ID; import static fiftyone.pipeline.did.FodIdTestFactory.TEST_DOMAIN; import static fiftyone.pipeline.did.FodIdTestFactory.canonicalPayload; @@ -77,7 +77,7 @@ public void tryFromBase64_ValidIdentifier_ParsedWithValue() FodId fodId = assertParsed(result); assertEquals(CANONICAL_FLAGS, fodId.getFlags()); assertEquals(CANONICAL_LICENSE_ID, fodId.getLicenseId()); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); assertEquals(TEST_DOMAIN, fodId.getDomain()); } @@ -93,7 +93,7 @@ public void tryFromBase64_UrlSafeUnpaddedWithWhitespace_Parsed() FodId.tryFromBase64(" " + urlSafe + "\r\n")); assertArrayEquals(fromUrlSafe.asByteArray(), fromSpaced.asByteArray()); - assertArrayEquals(CANONICAL_HASH, fromUrlSafe.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fromUrlSafe.getMatchKey()); } @Test @@ -103,7 +103,7 @@ public void tryFromByteArray_ValidIdentifier_ParsedWithValue() FodId fodId = assertParsed(FodId.tryFromByteArray(bytes)); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); assertArrayEquals(bytes, fodId.asByteArray()); } @@ -126,15 +126,15 @@ public void tryFromBase64_LongerSelfHostedDomain_Parsed() @Test public void tryFromBase64_LongerContextSection_Parsed() throws Exception { - // A payload longer than the value carries a creator context section + // A payload longer than the match key carries a creator context section // whose shape belongs to the cloud, so an older reader accepts it - // and exposes it as the payload beyond the value. + // and exposes it as the payload beyond the match key. byte[] payload = canonicalPayloadWithSection(512); String base64 = factory.signedOwidAt(payload, DATE).asBase64(); FodId fodId = assertParsed(FodId.tryFromBase64(base64)); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); assertArrayEquals(payload, fodId.getPayload()); assertTrue(fodId.verify(factory.publicPem)); } @@ -150,7 +150,7 @@ public void tryFromByteArray_MuchLongerPayload_NotRejectedForLength() FodId fodId = assertParsed(FodId.tryFromByteArray(bytes)); assertEquals(payload.length, fodId.getPayload().length); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); } byte[] random = Arrays.copyOf(canonicalRandomPayload(), 700); assertParsed(FodId.tryFromByteArray( @@ -183,7 +183,7 @@ public void tryFromBase64_ReservedHeaderOnly_ParsedBestEffort() factory.signedOwidAt(payload, DATE).asBase64())); assertEquals(IdType.RESERVED, fodId.getType()); - assertEquals(0, fodId.getHash().length); + assertEquals(0, fodId.getMatchKey().length); } // ----- 51Did payload rules ----- diff --git a/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdTestFactory.java b/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdTestFactory.java index f02d6c5d3..e92f6e77e 100644 --- a/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdTestFactory.java +++ b/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdTestFactory.java @@ -53,7 +53,7 @@ final class FodIdTestFactory { static final long CANONICAL_LICENSE_ID = 0x12345678L; - static final byte[] CANONICAL_HASH = canonicalHash(); + static final byte[] CANONICAL_MATCH_KEY = canonicalMatchKey(); static final Instant DATE_ORIGIN = Instant.parse("2020-01-01T00:00:00Z"); @@ -73,12 +73,12 @@ final class FodIdTestFactory { this.creator = Creator.create(TEST_DOMAIN, crypto); } - private static byte[] canonicalHash() { - byte[] hash = new byte[FodId.HASH_LENGTH]; - for (int i = 0; i < hash.length; i++) { - hash[i] = (byte) (0x20 + i); + private static byte[] canonicalMatchKey() { + byte[] matchKey = new byte[FodId.HASH_LENGTH]; + for (int i = 0; i < matchKey.length; i++) { + matchKey[i] = (byte) (0x20 + i); } - return hash; + return matchKey; } static byte[] canonicalPayload() { @@ -86,7 +86,8 @@ static byte[] canonicalPayload() { payload[FodId.FLAGS_OFFSET] = (byte) CANONICAL_FLAGS; writeCanonicalLicenseId(payload); System.arraycopy( - CANONICAL_HASH, 0, payload, FodId.HASH_OFFSET, FodId.HASH_LENGTH); + CANONICAL_MATCH_KEY, 0, payload, + FodId.HASH_OFFSET, FodId.HASH_LENGTH); return payload; } diff --git a/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdTests.java b/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdTests.java index b78a276ee..9afdce4c9 100644 --- a/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdTests.java +++ b/pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdTests.java @@ -35,7 +35,7 @@ import java.util.Base64; import static fiftyone.pipeline.did.FodIdTestFactory.CANONICAL_FLAGS; -import static fiftyone.pipeline.did.FodIdTestFactory.CANONICAL_HASH; +import static fiftyone.pipeline.did.FodIdTestFactory.CANONICAL_MATCH_KEY; import static fiftyone.pipeline.did.FodIdTestFactory.CANONICAL_LICENSE_ID; import static fiftyone.pipeline.did.FodIdTestFactory.TEST_DOMAIN; import static fiftyone.pipeline.did.FodIdTestFactory.canonicalPayload; @@ -85,7 +85,7 @@ public void constructor_FromBase64_UnpacksAllThreeFields() throws Exception { assertEquals(CANONICAL_FLAGS, fodId.getFlags()); assertEquals(CANONICAL_LICENSE_ID, fodId.getLicenseId()); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); assertEquals(TEST_DOMAIN, fodId.getDomain()); } @@ -98,7 +98,7 @@ public void constructor_FromBytes_UnpacksAllThreeFields() throws Exception { assertEquals(CANONICAL_FLAGS, fodId.getFlags()); assertEquals(CANONICAL_LICENSE_ID, fodId.getLicenseId()); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); assertEquals(TEST_DOMAIN, fodId.getDomain()); } @@ -110,7 +110,7 @@ public void constructor_FromOwid_UnpacksAllThreeFields() throws Exception { assertEquals(CANONICAL_FLAGS, fodId.getFlags()); assertEquals(CANONICAL_LICENSE_ID, fodId.getLicenseId()); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); assertEquals(owid.getDomain(), fodId.getDomain()); assertEquals(owid.getDate(), fodId.getDate()); assertEquals(owid.getVersion(), fodId.getVersion()); @@ -185,16 +185,28 @@ public void flags_AllBitsSet_Exposed() throws Exception { } @Test - public void hash_IsDefensiveCopy() throws Exception { + public void matchKey_IsDefensiveCopy() throws Exception { FodId fodId = FodId.fromBase64(factory.signedOwidBase64(canonicalPayload())); - byte[] hash = fodId.getHash(); - hash[0] = 0x00; - hash[FodId.HASH_LENGTH - 1] = 0x00; + byte[] matchKey = fodId.getMatchKey(); + matchKey[0] = 0x00; + matchKey[FodId.HASH_LENGTH - 1] = 0x00; - // Neither the underlying payload nor a fresh getHash() is affected. - assertEquals(CANONICAL_HASH[0], fodId.getPayload()[FodId.HASH_OFFSET]); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + // Neither the underlying payload nor a fresh getMatchKey() is affected. + assertEquals( + CANONICAL_MATCH_KEY[0], fodId.getPayload()[FodId.HASH_OFFSET]); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); + } + + @Test + @SuppressWarnings("deprecation") + public void getHash_DeprecatedAlias_ReturnsMatchKey() throws Exception { + // getHash() stays as a deprecated alias so that callers written + // against the earlier name keep compiling and get the same bytes. + FodId fodId = FodId.fromBase64(factory.signedOwidBase64(canonicalPayload())); + + assertArrayEquals(fodId.getMatchKey(), fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getHash()); } @Test @@ -238,8 +250,8 @@ public void constructor_PayloadLargerThanSpec_UsesFirst37Bytes() throws Exceptio assertEquals(CANONICAL_FLAGS, fodId.getFlags()); assertEquals(CANONICAL_LICENSE_ID, fodId.getLicenseId()); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); - assertEquals(FodId.HASH_LENGTH, fodId.getHash().length); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); + assertEquals(FodId.HASH_LENGTH, fodId.getMatchKey().length); } @Test @@ -262,7 +274,7 @@ public void constructor_LongContextSectionAndLongDomain_Parses() for (FodId fodId : new FodId[] { fromBytes, fromBase64, fromOwid }) { assertEquals(longDomain, fodId.getDomain()); assertEquals(CANONICAL_FLAGS, fodId.getFlags()); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); assertArrayEquals(payload, fodId.getPayload()); assertTrue(fodId.verify(factory.publicPem)); } @@ -281,7 +293,7 @@ public void base64Roundtrip_PreservesAllFields() throws Exception { assertEquals(fodId1.getFlags(), fodId2.getFlags()); assertEquals(fodId1.getLicenseId(), fodId2.getLicenseId()); - assertArrayEquals(fodId1.getHash(), fodId2.getHash()); + assertArrayEquals(fodId1.getMatchKey(), fodId2.getMatchKey()); assertEquals(fodId1.getDomain(), fodId2.getDomain()); } @@ -311,12 +323,12 @@ public void constructor_RandomPayload21Bytes_Parses() throws Exception { FodId fodId = FodId.fromBase64(factory.signedOwidBase64(canonicalRandomPayload())); assertEquals(CANONICAL_LICENSE_ID, fodId.getLicenseId()); - assertEquals(FodId.GUID_LENGTH, fodId.getHash().length); + assertEquals(FodId.GUID_LENGTH, fodId.getMatchKey().length); byte[] expected = new byte[FodId.GUID_LENGTH]; for (int i = 0; i < expected.length; i++) { expected[i] = (byte) (0x40 + i); } - assertArrayEquals(expected, fodId.getHash()); + assertArrayEquals(expected, fodId.getMatchKey()); } @Test @@ -328,7 +340,7 @@ public void constructor_RandomPayloadOneByteShort_Throws() throws Exception { } @Test - public void constructor_RandomPayloadLargerThanSpec_UsesFirst16ValueBytes() + public void constructor_RandomPayloadLargerThanSpec_UsesFirst16MatchKeyBytes() throws Exception { byte[] payload = new byte[FodId.PAYLOAD_LENGTH]; System.arraycopy( @@ -340,7 +352,7 @@ public void constructor_RandomPayloadLargerThanSpec_UsesFirst16ValueBytes() FodId fodId = FodId.fromBase64(factory.signedOwidBase64(payload)); assertEquals(IdType.RANDOM, fodId.getType()); - assertEquals(FodId.GUID_LENGTH, fodId.getHash().length); + assertEquals(FodId.GUID_LENGTH, fodId.getMatchKey().length); } @Test @@ -360,13 +372,13 @@ public void constructor_ReservedHeaderOnly_Parses() throws Exception { FodId fodId = FodId.fromBase64(factory.signedOwidBase64(payload)); assertEquals(IdType.RESERVED, fodId.getType()); - assertEquals(0, fodId.getHash().length); + assertEquals(0, fodId.getMatchKey().length); } // ----- Gap tests (runbook section 6b) ----- @Test - public void compareTwo51Dids_SamePayload_SameValueDifferentEnvelopes() + public void compareTwo51Dids_SamePayload_SameMatchKeyDifferentEnvelopes() throws Exception { byte[] payload = canonicalPayload(); // The creator stamps "now" to the minute, so two reissues at @@ -379,8 +391,8 @@ public void compareTwo51Dids_SamePayload_SameValueDifferentEnvelopes() FodId fodA = FodId.fromBase64(a.asBase64()); FodId fodB = FodId.fromBase64(b.asBase64()); - // The value is stable across reissues... - assertArrayEquals(fodA.getHash(), fodB.getHash()); + // The match key is stable across reissues... + assertArrayEquals(fodA.getMatchKey(), fodB.getMatchKey()); // ...while the envelope differs. assertNotEquals(fodA.getDate(), fodB.getDate()); assertFalse(Arrays.equals(fodA.getSignature(), fodB.getSignature())); @@ -402,7 +414,7 @@ public void construction_DoesNotVerify() throws Exception { assertEquals(CANONICAL_FLAGS, fodId.getFlags()); assertEquals(CANONICAL_LICENSE_ID, fodId.getLicenseId()); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); } @Test @@ -413,7 +425,7 @@ public void fromOwid_HoldsTheEnvelopeItWasGiven() throws Exception { FodId fodId = FodId.fromOwid(owid); assertEquals(CANONICAL_FLAGS, fodId.getFlags()); - assertArrayEquals(CANONICAL_HASH, fodId.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fodId.getMatchKey()); assertArrayEquals(owid.asByteArray(), fodId.asByteArray()); } @@ -434,7 +446,7 @@ public void roundtrip_ThroughBytesConstructor_PreservesAllFields() assertEquals(fodId1.getFlags(), fodId2.getFlags()); assertEquals(fodId1.getLicenseId(), fodId2.getLicenseId()); - assertArrayEquals(fodId1.getHash(), fodId2.getHash()); + assertArrayEquals(fodId1.getMatchKey(), fodId2.getMatchKey()); assertEquals(fodId1.getDomain(), fodId2.getDomain()); } @@ -456,7 +468,7 @@ public void fromBase64_AcceptsStandardUrlSafeAndUnpadded() throws Exception { assertArrayEquals(fromStandard.asByteArray(), fromUrlSafe.asByteArray()); assertArrayEquals(fromStandard.asByteArray(), fromUnpadded.asByteArray()); - assertArrayEquals(CANONICAL_HASH, fromUnpadded.getHash()); + assertArrayEquals(CANONICAL_MATCH_KEY, fromUnpadded.getMatchKey()); } @Test From af105ad3970ea99f100d51a7a507cd0b908e7796 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 31 Aug 2026 16:17:17 +0100 Subject: [PATCH 3/3] Use the match key vocabulary in the 51Did developer example The offline example reads the match key with getMatchKey(), prints it under that name, and its README, comments and self check describe the match key as the stable part while the envelope changes. --- .../README.md | 2 +- .../developerexamples/fodid/Main.java | 27 ++++++++++--------- .../developerexamples/fodid/ExampleTests.java | 6 ++--- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/pipeline.developer-examples/pipeline.developer-examples.fodid/README.md b/pipeline.developer-examples/pipeline.developer-examples.fodid/README.md index e7cb93bf7..62cd57a7b 100644 --- a/pipeline.developer-examples/pipeline.developer-examples.fodid/README.md +++ b/pipeline.developer-examples/pipeline.developer-examples.fodid/README.md @@ -6,7 +6,7 @@ This module holds the developer examples for the 51Did package | Program | What it shows | | --- | --- | -| `Main` | Reads a 51Did offline. Builds a sample identifier in process, parses it back with `FodId` and shows that the value is stable while the envelope changes on every issue. Needs no cloud access. | +| `Main` | Reads a 51Did offline. Builds a sample identifier in process, parses it back with `FodId` and shows that the match key is stable while the envelope changes on every issue. Needs no cloud access. | | `CreatorContextDemoServer` | Serves a small web page that creates a 51Did in the browser, verifies it from the browser, and redeems the encrypted creator context result on this server with `DidClient`, which is the only place the licence key lives. | ## Creator context diff --git a/pipeline.developer-examples/pipeline.developer-examples.fodid/src/main/java/pipeline/developerexamples/fodid/Main.java b/pipeline.developer-examples/pipeline.developer-examples.fodid/src/main/java/pipeline/developerexamples/fodid/Main.java index 1e98446fa..7e82805eb 100644 --- a/pipeline.developer-examples/pipeline.developer-examples.fodid/src/main/java/pipeline/developerexamples/fodid/Main.java +++ b/pipeline.developer-examples/pipeline.developer-examples.fodid/src/main/java/pipeline/developerexamples/fodid/Main.java @@ -37,8 +37,8 @@ * back with {@link FodId} and prints the three payload fields. *

* It also demonstrates 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. + * every call (the envelope, hence the base64, changes), but the match key + * is stable. Compare match keys, never envelopes. */ public class Main { @@ -62,28 +62,29 @@ public void run() throws Exception { System.out.println(" Flags : 0x" + Integer.toHexString(fodId.getFlags())); System.out.println(" LicenseId : " + fodId.getLicenseId()); - System.out.println(" Hash : " + toHex(fodId.getHash())); + System.out.println(" Match key : " + toHex(fodId.getMatchKey())); System.out.println(" Verifies : " + fodId.verify(crypto.publicKeyPem())); - // Issue the SAME payload again: a separate envelope, same value. + // Issue the SAME payload again: a separate envelope, same match + // key. FodId reissued = FodId.fromBase64(issue(creator, payload)); boolean sameEnvelope = fodId.asBase64().equals(reissued.asBase64()); - boolean sameValue = - Arrays.equals(fodId.getHash(), reissued.getHash()); + boolean sameMatchKey = + Arrays.equals(fodId.getMatchKey(), reissued.getMatchKey()); System.out.println(); System.out.println("Same payload, re-issued:"); System.out.println(" Same envelope (base64) : " + sameEnvelope); - System.out.println(" Same value (Hash) : " + sameValue); + System.out.println(" Same match key : " + sameMatchKey); - // The reader's whole purpose: the value is the stable, comparable - // part while the envelope is not. - if (sameEnvelope || !sameValue) { + // The reader's whole purpose: the match key is the stable, + // comparable part while the envelope is not. + if (sameEnvelope || !sameMatchKey) { throw new IllegalStateException( - "Expected a different envelope but the same value " - + "across reissues."); + "Expected a different envelope but the same match " + + "key across reissues."); } } @@ -99,7 +100,7 @@ private String issue(Creator creator, byte[] 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. */ private byte[] samplePayload() { byte[] payload = new byte[FodId.PAYLOAD_LENGTH]; diff --git a/pipeline.developer-examples/pipeline.developer-examples.fodid/src/test/java/pipeline/developerexamples/fodid/ExampleTests.java b/pipeline.developer-examples/pipeline.developer-examples.fodid/src/test/java/pipeline/developerexamples/fodid/ExampleTests.java index 9451e81fc..7f37bc31e 100644 --- a/pipeline.developer-examples/pipeline.developer-examples.fodid/src/test/java/pipeline/developerexamples/fodid/ExampleTests.java +++ b/pipeline.developer-examples/pipeline.developer-examples.fodid/src/test/java/pipeline/developerexamples/fodid/ExampleTests.java @@ -72,8 +72,8 @@ public void init() throws Exception { /** * The 51Did example is fully offline, so unlike the cloud examples it must * complete without throwing. {@code run()} also self-checks the - * value-stable / envelope-changes invariant and throws if it does not - * hold. + * invariant that the match key is stable while the envelope changes, + * and throws if it does not hold. */ @Test public void FodId_Example_Test() throws Exception { @@ -241,7 +241,7 @@ private static String keyList(Crypto crypto) { /** * 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. */ private static byte[] samplePayload() { byte[] payload = new byte[FodId.PAYLOAD_LENGTH];