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
55 changes: 54 additions & 1 deletion fodid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,47 @@ which determines the length and meaning of the match key:
- `IdType::Random` carries a 16-byte server-generated GUID.
- `IdType::Reserved` is not yet assigned and is parsed best effort.

## The terms the identifier was created under

The byte after the match key says which terms document the 51Did was created
under, so that the terms travel with the identifier rather than beside it. It
is an index into a table published in the
[specification](https://github.com/51Degrees/specifications/blob/main/did-specification/identifier-layout.md)
and is not a version number. `FodId::terms` answers with the address of the
document, so a caller never handles the byte.

| Index | Document | `FodId::terms` |
| --- | --- | --- |
| `0` | Not stated in the identifier | `None` |
| `1` | Model Terms for Marketing, version 2 | `Some("https://m4ow.uk/mtm/2.txt")` |
| any other | One this crate cannot name | `None` |

An identifier whose payload ends at the match key carries no terms byte, and
a missing byte is index 0, which answers with no address. An index added to
the specification after this release answers with no address as well, and no
address is ever built from an index this crate cannot name, since that would
name a document nobody wrote. A caller therefore cannot tell an index of
zero from an index this crate cannot name, which is deliberate, because both
lead to the same place. This crate answers with the address and never
fetches it.

## The payload version

Bits 4 and 5 of the flags byte say which payload layout the identifier
follows, and this crate reads version 0. A payload naming version 1, 2 or 3
is refused with `Error::UnsupportedPayloadVersion`, which names the version
it found.

No field is read under the layout this crate knows once the version says
otherwise. A later version exists precisely because a field moved, so
reading such a payload here would answer with values that are wrong rather
than absent, which is worse than refusing. A version that nothing checks
protects nothing.

The version is not exposed. Either this crate read the layout, in which case
the accessors are the answer, or it did not, in which case there is no
identifier to read fields from.

## Payload layout

The payload is a five byte header, being a flags byte and a four byte little
Expand All @@ -62,10 +103,17 @@ and the accessors every 51Did package offers at
[package-surface.md](https://github.com/51Degrees/specifications/blob/main/did-specification/package-surface.md),
and those two pages are the authority rather than any summary here.

A terms byte follows the match key, read through `FodId::terms`, which
answers with the address of the document the identifier was created under.
Where it sits depends on the match key length the type requires, which is
one reason the offsets stay internal.

The lengths given there are lower bounds. The payload must hold the header
before the type can be read, and then the value the type requires, being 16
GUID bytes for a random identifier and 32 hash bytes for a probabilistic or
hashed email one. A payload may carry more bytes after the value, which this
hashed email one. The terms byte follows the value, so where it sits depends
on the value length the type requires, and a payload that ends at the value
carries none. A payload may carry more bytes after the terms, which this
crate accepts and leaves in place. There is no upper bound on a 51Did in this
crate, so a reader built today keeps reading identifiers issued in a newer,
longer shape.
Expand Down Expand Up @@ -95,6 +143,10 @@ fn read(base64_from_cloud_service: &str, public_pem: &str) -> Result<(), fodid::
let license_id = fod_id.license_id(); // u32
let match_key = fod_id.match_key(); // the match key bytes (SHA-256 or GUID)

// The terms the identifier was created under, and the address of that
// document where this crate knows the index.
let terms = fod_id.terms(); // Option<&'static str>

// Inherited OWID level fields and operations, available through Deref.
let domain = fod_id.domain();
let round_trip = fod_id.as_base64()?;
Expand All @@ -105,6 +157,7 @@ fn read(base64_from_cloud_service: &str, public_pem: &str) -> Result<(), fodid::

let _ = (usage, from_consent, id_type, license_id, match_key);
let _ = (domain, round_trip, genuine);
let _ = terms;
Ok(())
}
```
Expand Down
26 changes: 23 additions & 3 deletions fodid/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ pub type Result<T> = std::result::Result<T, Error>;
/// outcome. Each one is a named status a caller can branch on directly,
/// without matching on message text, and together they are the 51Did status
/// vocabulary, being the OWID one (carried unchanged inside
/// [`Error::Parse`]) plus the two 51Did statuses [`Error::PayloadTooShort`]
/// and [`Error::InvalidTypePayloadLength`].
/// [`Error::Parse`]) plus the three 51Did statuses
/// [`Error::PayloadTooShort`], [`Error::InvalidTypePayloadLength`] and
/// [`Error::UnsupportedPayloadVersion`].
///
/// A successful read says nothing about the signature. Whether the bytes
/// are a 51Did and whether the signature is genuine are two questions with
Expand Down Expand Up @@ -81,6 +82,18 @@ pub enum Error {
/// The number of payload bytes actually present.
actual: usize,
},
/// Bits 4 and 5 of the flags byte name a payload layout version this
/// crate does not know, so no field is read.
///
/// A later version exists precisely because a field moved, so reading
/// the payload under the layout this crate knows would answer with
/// values that are wrong rather than absent, which is worse than
/// refusing.
UnsupportedPayloadVersion {
/// The version the payload named, being 1, 2 or 3, since 0 is the
/// layout this crate reads.
version: u8,
},
/// An OWID operation other than a read failed, for example serialising
/// the envelope again or verifying its signature. Wraps the error type of
/// the OWID library compiled into this crate, re-exported as
Expand Down Expand Up @@ -108,6 +121,11 @@ impl fmt::Display for Error {
"InvalidTypePayloadLength: a {id_type:?} 51Did needs at least \
{expected} payload bytes and {actual} are present"
),
Error::UnsupportedPayloadVersion { version } => write!(
f,
"UnsupportedPayloadVersion: 51Did payload version {version} \
is not one this crate can read"
),
Error::Owid(e) => write!(f, "OWID operation failed because {e}"),
}
}
Expand All @@ -118,7 +136,9 @@ impl std::error::Error for Error {
match self {
Error::Parse(e) => Some(e),
Error::Owid(e) => Some(e),
Error::PayloadTooShort { .. } | Error::InvalidTypePayloadLength { .. } => None,
Error::PayloadTooShort { .. }
| Error::InvalidTypePayloadLength { .. }
| Error::UnsupportedPayloadVersion { .. } => None,
}
}
}
Expand Down
Loading
Loading