Documentation
¶
Overview ¶
Package odp implements the Offline Delivery Protocol (ODP) from INTERCONNECTION.md.
ODP wraps each datum ("packet") sent from a source server to a target server in a signed, MessagePack-encoded envelope. It does not define transport: an ODP packet is a self-contained, one-time-use token that can be handed through any channel (a Minecraft cookie, an HTTP body, a file …) and independently verified by the recipient.
Signing ¶
The ed25519 signature covers a purpose-built message, not the MessagePack envelope:
M = version || len(target) || target || time || until ||
len(action) || action || len(subject) || subject || SHA3-224(data)
where len(x) is the VarInt-encoded byte length of x, integers are big-endian, and absent optional fields (a nil Subject or Data) are skipped together with their length prefix. See Packet.SigningMessage.
Optional fields ¶
Subject and Data are optional. A nil slice means "absent" (the field is omitted from the envelope and skipped while signing); a non-nil slice — even an empty one — means "present".
Verifying and replay ¶
Packet.Verify performs every check the specification mandates, including rejecting replays. Because ODP packets are single-use, verification requires a ReplayGuard whose record must outlive the packet's expiry and survive restarts. MemoryReplayGuard is provided for tests and single-process use; production deployments should back the guard with durable storage.
MessagePack canonicalization ¶
The specification names MessagePack as the envelope format but does not pin a canonical form. Two consequences, and this package's choices:
- The signature covers M (above), not the MessagePack bytes, so re-encoding the envelope cannot invalidate a packet.
- The one place the encoding itself is hashed is SHA-224(data): for a Redirection Token, data is the MessagePack-encoded PlayerProfile. Verify hashes the bytes as received (it never re-encodes), so a Go issuer and a Go verifier always agree regardless of encoder details. For cross-language interoperability, this library encodes integers with the smallest format that fits (canonical minimal width) — the default of mainstream MessagePack libraries — so an issuer on another platform that does the same produces byte-identical data. See internal/msgpack.
Example ¶
Seal an ODP packet, encode it, then decode and verify it. A second verification with the same replay guard is rejected because ODP packets are single-use.
package main
import (
"bytes"
"fmt"
"time"
"github.com/icebear67/mfp-go"
"github.com/icebear67/mfp-go/odp"
)
func main() {
issuer, _ := mfp.IdentityFromSeed(bytes.Repeat([]byte{1}, mfp.SeedSize))
recipient, _ := mfp.IdentityFromSeed(bytes.Repeat([]byte{2}, mfp.SeedSize))
now := time.Unix(1_700_000_000, 0)
packet, _ := odp.Seal(issuer, odp.Content{
Target: recipient.Public(),
Time: now.Add(-time.Minute),
Until: now.Add(time.Hour),
Action: "ticket",
Subject: []byte("player-uuid"),
Data: []byte("payload"),
})
wire, _ := packet.Marshal()
decoded, _ := odp.Unmarshal(wire)
opts := odp.VerifyOptions{
Known: mfp.NewKeySet(issuer.Public()),
Replay: odp.NewMemoryReplayGuard(),
Recipient: recipient.Public(),
Now: func() time.Time { return now },
}
fmt.Println("action:", decoded.Action)
fmt.Println("first verify:", decoded.Verify(opts))
fmt.Println("second verify:", decoded.Verify(opts))
}
Output: action: ticket first verify: <nil> second verify: odp: replayed packet
Index ¶
Examples ¶
Constants ¶
const ( FieldVersion = "version" FieldIssuer = "issuer" FieldTarget = "target" FieldTime = "time" FieldUntil = "until" FieldAction = "action" FieldSubject = "subject" FieldData = "data" FieldSignature = "signature" )
Envelope field names, as used as MessagePack map keys.
Variables ¶
var ( // ErrVersion means the version field is not [mfp.ODPVersion]. ErrVersion = errors.New("odp: unsupported version") // ErrMissingField means a required field is absent. ErrMissingField = errors.New("odp: missing required field") // ErrTimeRange means time >= until (the validity window is empty). ErrTimeRange = errors.New("odp: time is not before until") // ErrNotYetValid means the current time is before the packet's time. ErrNotYetValid = errors.New("odp: packet not yet valid") // ErrExpired means the current time is at or after the packet's until. ErrExpired = errors.New("odp: packet expired") // ErrUnknownIssuer means the issuer is not in the known key set. ErrUnknownIssuer = errors.New("odp: unknown issuer") // ErrWrongTarget means the packet's target is not the expected recipient. ErrWrongTarget = errors.New("odp: packet addressed to a different target") // ErrBadSignature means the ed25519 signature did not verify. ErrBadSignature = errors.New("odp: invalid signature") // ErrReplay means the signature was already recorded by the replay guard. ErrReplay = errors.New("odp: replayed packet") )
Verification errors. Use errors.Is to test for a specific cause.
Functions ¶
This section is empty.
Types ¶
type Content ¶
type Content struct {
Target mfp.PublicKey
Time time.Time
Until time.Time
Action string
Subject []byte // optional (nil = absent)
Data []byte // optional (nil = absent)
}
Content is the caller-supplied part of a packet, before it is signed by Seal. Version, Issuer and Signature are filled in by Seal.
type MemoryReplayGuard ¶
type MemoryReplayGuard struct {
// contains filtered or unexported fields
}
MemoryReplayGuard is an in-memory ReplayGuard suitable for tests and single-process deployments that can tolerate losing replay state on restart. It is safe for concurrent use.
It is NOT persistent: restarting the process forgets every recorded signature, which violates the specification's durability requirement. Use a storage-backed guard in production.
func NewMemoryReplayGuard ¶
func NewMemoryReplayGuard() *MemoryReplayGuard
NewMemoryReplayGuard returns an empty in-memory guard.
func (*MemoryReplayGuard) Len ¶
func (g *MemoryReplayGuard) Len() int
Len reports how many signatures are currently recorded.
func (*MemoryReplayGuard) Purge ¶
func (g *MemoryReplayGuard) Purge(now time.Time) int
Purge drops every recorded signature whose expiry is at or before now, returning how many were removed. Call it periodically to bound memory.
func (*MemoryReplayGuard) Seen ¶
Seen implements ReplayGuard.
type Packet ¶
type Packet struct {
Version uint8 // constant [mfp.ODPVersion]
Issuer mfp.PublicKey // signer's ed25519 public key (32 bytes)
Target mfp.PublicKey // recipient's ed25519 public key (32 bytes)
Time time.Time // issued-at
Until time.Time // expiry
Action string // purpose, e.g. "redirect"
Subject []byte // optional; typically a player UUID
Data []byte // optional; action-specific payload
Signature []byte // ed25519 signature (64 bytes)
}
Packet is a decoded ODP envelope. Times are stored with second precision, as on the wire (UNIX seconds).
Subject and Data are optional: a nil slice is absent, a non-nil slice is present (see the package documentation).
func Seal ¶
Seal fills in Version, Issuer and Signature for the given content and returns the finished packet. It requires content.Time < content.Until.
func Unmarshal ¶
Unmarshal decodes a MessagePack ODP envelope. It validates only structural well-formedness; call Packet.Verify to authenticate the packet.
func (*Packet) Marshal ¶
Marshal encodes the packet as a MessagePack envelope. Absent optional fields (nil Subject/Data) are omitted from the map.
func (*Packet) SigningMessage ¶
SigningMessage builds the message M whose ed25519 signature is stored in Signature, exactly as specified in INTERCONNECTION.md.
func (*Packet) Verify ¶
func (p *Packet) Verify(opts VerifyOptions) error
Verify runs every check ODP mandates before a packet may be trusted:
- version == mfp.ODPVersion;
- time < until and time < now < until;
- all required fields are present and well-formed;
- issuer is a known federation key;
- the signature over M verifies against issuer;
- the signature has not been seen before (when a ReplayGuard is given).
On success with a guard, the signature is atomically recorded as used.
type ReplayGuard ¶
type ReplayGuard interface {
// Seen reports whether signature was recorded before this call. If it was
// not, Seen records it (with the given expiry, so the record may later be
// purged) and returns false. A true result means the packet is a replay.
Seen(signature []byte, expiry time.Time) (bool, error)
}
ReplayGuard enforces the single-use property of ODP packets. Verification consults it as the final step: a signature that has already been recorded is a replay and must be rejected.
Implementations MUST:
- be atomic — ReplayGuard.Seen checks and records in one step, so that two concurrent verifications of the same packet cannot both succeed;
- persist records at least until each signature's expiry, across process restarts (the specification requires the record to survive reboots).
type VerifyOptions ¶
type VerifyOptions struct {
// Known is the registry of authorized federation public keys. The issuer
// must be a member. Required.
Known *mfp.KeySet
// Replay records accepted signatures and rejects repeats. If nil, the
// single-use replay check (step 6) is skipped — verification then only
// proves authenticity, not freshness, and the caller is responsible for
// preventing reuse. For spec compliance, supply a durable guard.
Replay ReplayGuard
// Recipient, when non-nil, additionally requires the packet's Target to
// equal it. Base ODP does not mandate this, but recipients almost always
// want it (and PIP requires it for Redirection Tokens).
Recipient mfp.PublicKey
// Now overrides the clock used for the validity-window check. Defaults to
// time.Now.
Now func() time.Time
}
VerifyOptions configures Packet.Verify.