odp

package
v0.0.0-...-ae481c8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 9 Imported by: 0

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

View Source
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

View Source
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

func (g *MemoryReplayGuard) Seen(signature []byte, expiry time.Time) (bool, error)

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

func Seal(issuer *mfp.Identity, content Content) (*Packet, error)

Seal fills in Version, Issuer and Signature for the given content and returns the finished packet. It requires content.Time < content.Until.

func Unmarshal

func Unmarshal(data []byte) (*Packet, error)

Unmarshal decodes a MessagePack ODP envelope. It validates only structural well-formedness; call Packet.Verify to authenticate the packet.

func (*Packet) Marshal

func (p *Packet) Marshal() ([]byte, error)

Marshal encodes the packet as a MessagePack envelope. Absent optional fields (nil Subject/Data) are omitted from the map.

func (*Packet) SigningMessage

func (p *Packet) SigningMessage() ([]byte, error)

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:

  1. version == mfp.ODPVersion;
  2. time < until and time < now < until;
  3. all required fields are present and well-formed;
  4. issuer is a known federation key;
  5. the signature over M verifies against issuer;
  6. 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL