Heatrail

Chapter 03

Identity without identities

A customer types an e-mail address into a checkout field. It goes into the shop's own order record, where it belongs, and a moment later the shop's server sends a check to Heatrail about that order. The address is not in the request, and there is no version of the request in which it could be. This article is about what travels instead, and what it protects.

The property

Raw e-mail addresses and phone numbers never reach Heatrail. What arrives is a keyed hash computed in the shop. What is stored is a second keyed hash computed only on the API, so the value in our database is not the value the shop sent. A hash of an e-mail address is still personal data, and we treat it as such.

Why it holds

Two stages on two machines

The first computation happens on the shop's own server. It takes the value the shop already holds, derives a keyed hash from it, and puts that hash in the request. The wording matters: this is not a promise that we discard the address after receiving it, and it is not a retention policy. The address never leaves the shop's infrastructure, so there is no misconfiguration on our side that could cause us to receive it, no debug flag that logs it and no support procedure that reads it back. A thing that never travels cannot arrive by accident.

The second computation happens only on the API, under key material that has never been anywhere else, and only its output is written down. So the values in our database are not the values any shop computed, and a copy of the database taken without that key material cannot be lined up against what shops send. The two stages exist for two different reasons. The first exists so that two shops can recognise the same identifier as each other without either of them showing it to us. The second exists so that our storage is not a set of values somebody else already holds.

Roles are kept apart on purpose, including one separation that is easy to miss: the credential a shop signs its requests with plays no part in identifier hashing. The constructions themselves, the field formats and what an integrator actually implements are documented where an integrator needs them, in the SDK reference and the API reference. This page is about the property, which is what stays true whichever way the mechanism is built.

Figure 1

Two machines, two keys. The value that crosses the wire is not the address, and the value that is stored is not the value that crossed the wire.On the left, a box labelled Your shop holds key A. Inside it, the customer's address arrives from above into a chip showing a sample address, an arrow leads to a second chip showing a first hashed value, and a note reads that the raw value stays on this machine. A dashed line from the address towards the right stops at a bar before it reaches the second machine, labelled the raw value never takes this path. A solid arrow crosses the gap carrying the first hashed value only. On the right, a box labelled Heatrail API holds key B. Inside it the first hashed value arrives, an arrow labelled not the same value leads to a second, different hashed value, and a note reads that only this second value is stored.The address as the customer typed it at your checkoutthe raw value never takes this pathYour shopkey Amari.tamm@example.eeh₁ 9f3a…keyed once here, with key AThe raw value stays on this machine.h₁ 9f3a…only this crossesHeatrail APIkey Bh₁ 9f3a…h₂ c4f1…not the same valueOnly this second value is stored.A copy of our database is not a contact list, and it is not the set of values your shop sent.
The value on the wire is not the address, and the value in the database is not the value on the wire. Neither machine holds both keys.

Empty is refused, not hashed

Most of the usefulness of a hashed identifier comes from a dull assumption: two different people produce two different values. That assumption breaks the moment somebody hashes nothing.

The temptation is real and well meant. A shop with no phone number would like the payload shape to stay constant, so it hashes the empty string, or whatever placeholder the checkout drops into an untouched field, and sends that. The result is one value shared by every guest checkout in the network, which the system would read as a single extraordinarily busy customer tying thousands of unrelated orders together across dozens of shops. It would be the largest and most confident piece of nonsense in the graph, arrived at through a helpful convenience.

So it is refused on both sides. The client function returns nothing for an empty value, a whitespace value, a known placeholder and anything it cannot parse, and the contract is that the field is then omitted from the request entirely rather than sent as an empty string. The API separately refuses known placeholder values on arrival, because a contract only becomes a boundary once one side of it stops assuming the other got it right. A shop with no phone number sends no phone field. That is the whole of it.

What hashing buys, and what it does not

What it buys is narrow and worth having. Somebody who took everything we store would hold a set of values that cannot be read, cannot be mailed and cannot be called. A compromise of Heatrail does not yield a contact list. That is the sentence the whole arrangement exists to make true.

What it does not do is make the data anonymous. A shop that already holds a customer's address can compute the first-stage value for it and see whether that value is among the ones it sent us. That is not a weakness bolted onto the design, it is the same property that makes cross-shop recognition work at all: a value anybody with the original can reproduce is pseudonymous, not anonymous. It is also the reason every article in this library treats a hashed identifier as personal data throughout, and the reason hashing appears here as a safeguard rather than as an exemption from anything.

One boundary follows from the first stage, and the security page states it in the same words. That stage is keyed with material shared across participating shops, because shared material is exactly what makes one person's identifier look the same to two of them. It therefore sits, by construction, on every participating shop's server, so cross-shop recognition is as strong as the custody of that material across the network. The second stage is what keeps Heatrail's own storage out of that dependency: it runs on the API alone, under key material that has never been anywhere else.

Verify it

Two checks. The first fails before authentication matters, because the identifier field accepts a hash and nothing else: a raw address is a validation error rather than a value we take in and then promise to forget.

# Sign this the way the reference shows, and send a real Idempotency-Key:
# both are checked before the payload is, so a placeholder signature answers
# 401 and a missing key answers 400 rather than the error being demonstrated.
curl -s -X POST https://api.heatrail.eu/v1/check \
  -H "Content-Type: application/json" \
  -H "Authorization: HR1 {key id}:{signature}" \
  -H "Idempotency-Key: chk-verify-hashing-boundary-01" \
  -d '{"order_ref":"ord-1","ip":"203.0.113.42",
       "email_h":"mari.tamm@example.ee"}'

# 422 invalid_payload. The details name email_h: the field takes a
# stage-one hash, and an address is not one.

The second is a read rather than a call. The client is published code, so open its identifier hashing function and confirm what it does with nothing: an empty value, a whitespace value and a known placeholder all come back as null, and the documented contract is that the field is then left out. If it returned a value there instead, the network would acquire the enormous shared node described above, which makes it the one function on this page most worth reading yourself. It is in the SDK reference, and the same guarantee is stated for the API in the API reference.

  • Trails, on what these values are linked into once they arrive, and why that is a property of history rather than a record about a person.
  • Why the network cannot be enumerated, on the second half of the same argument: a hashed identifier is little use to an attacker who also cannot ask us questions about it.
  • Forgetting on schedule, on how long these values are kept and what deletes them.
  • The SDK reference, for the mechanism this article deliberately describes only as a property.

Heatrail

This site is not public yet

Heatrail is in a private pre-launch period. Enter the access password to continue.

Enquiries: hello@heatrail.eu