documents

package
v0.99.2 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package documents defines AuthKit's generic immutable signed-document wire contract. It authenticates transport metadata and opaque JSON payload bytes; application schema and authorization remain the receiving application's job.

Index

Examples

Constants

View Source
const (
	JOSEType = "authkit-document+jws"

	MaxTypeBytes           = 256
	MaxIssuerBytes         = 2048
	MaxAudienceBytes       = 512
	MaxAudiences           = 32
	MaxPayloadBytes        = 1 << 20
	MaxSignedPayloadBytes  = MaxPayloadBytes + 16<<10
	MaxCompactJWSBytes     = 2 << 20
	MaxReferences          = 16
	MaxReferencesJSONBytes = 4 << 10
)
View Source
const PublicationPathPrefix = "/.well-known/authkit/documents/"

Variables

View Source
var (
	CodeInvalidReference     = errmodel.Define("invalid_document_reference", 400, "The document reference is invalid.")
	CodeInvalidType          = errmodel.Define("invalid_document_type", 400, "The document type is invalid.")
	CodeInvalidDigest        = errmodel.Define("invalid_document_digest", 400, "The document digest is invalid.")
	CodeDuplicateReference   = errmodel.Define("duplicate_document_reference", 400, "The document reference is duplicated.")
	CodeTooManyReferences    = errmodel.Define("too_many_document_references", 400, "Too many document references.")
	CodeReferencesTooLarge   = errmodel.Define("document_references_too_large", 400, "The document references are too large.")
	CodeWrongTokenType       = errmodel.Define("documents_wrong_token_type", 400, "The token type is wrong for documents.")
	CodeReservedAttribute    = errmodel.Define("reserved_document_attribute", 400, "The document uses a reserved attribute.")
	CodeInvalidEnvelope      = errmodel.Define("invalid_document_envelope", 400, "The document envelope is invalid.")
	CodePayloadTooLarge      = errmodel.Define("document_payload_too_large", 400, "The document payload is too large.")
	CodeMalformedJWS         = errmodel.Define("malformed_document_jws", 400, "The document JWS is malformed.")
	CodeWrongJOSEType        = errmodel.Define("wrong_document_jose_type", 400, "The document JOSE type is wrong.")
	CodeUnsupportedAlgorithm = errmodel.Define("unsupported_document_algorithm", 400, "The document algorithm is not supported.")
	CodeUnsupportedSigner    = errmodel.Define("unsupported_document_signer", 400, "The document signer is not supported.")
	CodeUnknownKey           = errmodel.Define("unknown_document_key", 400, "The document names an unknown key.")
	CodeInvalidSignature     = errmodel.Define("invalid_document_signature", 400, "The document signature is invalid.")
	CodeDigestMismatch       = errmodel.Define("document_digest_mismatch", 400, "The document digest does not match.")
	CodeIssuerMismatch       = errmodel.Define("document_issuer_mismatch", 400, "The document issuer does not match.")
	CodeAudienceMismatch     = errmodel.Define("document_audience_mismatch", 400, "The document audience does not match.")
	CodeTypeMismatch         = errmodel.Define("document_type_mismatch", 400, "The document type does not match.")
	CodeUntrustedIssuer      = errmodel.Define("untrusted_document_issuer", 403, "The document issuer is not trusted.")
	CodeUnauthorized         = errmodel.Define("document_unauthorized", 401, "The document request is not authorized.")
	CodeNotFound             = errmodel.Define("document_not_found", 404, "The document was not found.")
	CodeFetch                = errmodel.Define("document_fetch_failed", 502, "The document could not be fetched.")
	CodeRedirect             = errmodel.Define("document_redirect_rejected", 502, "The document redirect was rejected.")
	CodeDigestCollision      = errmodel.Define("document_digest_collision", 409, "A different document already exists under that digest.")
)

Document error codes on the one AuthKit error model (ak#290): each sentinel is an *errmodel.Error (= authkit.Error) with its catalogued status.

View Source
var (
	ErrInvalidReference     = errmodel.E(CodeInvalidReference)
	ErrInvalidType          = errmodel.E(CodeInvalidType)
	ErrInvalidDigest        = errmodel.E(CodeInvalidDigest)
	ErrDuplicateReference   = errmodel.E(CodeDuplicateReference)
	ErrTooManyReferences    = errmodel.E(CodeTooManyReferences)
	ErrReferencesTooLarge   = errmodel.E(CodeReferencesTooLarge)
	ErrWrongTokenType       = errmodel.E(CodeWrongTokenType)
	ErrReservedAttribute    = errmodel.E(CodeReservedAttribute)
	ErrInvalidEnvelope      = errmodel.E(CodeInvalidEnvelope)
	ErrPayloadTooLarge      = errmodel.E(CodePayloadTooLarge)
	ErrMalformedJWS         = errmodel.E(CodeMalformedJWS)
	ErrWrongJOSEType        = errmodel.E(CodeWrongJOSEType)
	ErrUnsupportedAlgorithm = errmodel.E(CodeUnsupportedAlgorithm)
	ErrUnsupportedSigner    = errmodel.E(CodeUnsupportedSigner)
	ErrUnknownKey           = errmodel.E(CodeUnknownKey)
	ErrInvalidSignature     = errmodel.E(CodeInvalidSignature)
	ErrDigestMismatch       = errmodel.E(CodeDigestMismatch)
	ErrIssuerMismatch       = errmodel.E(CodeIssuerMismatch)
	ErrAudienceMismatch     = errmodel.E(CodeAudienceMismatch)
	ErrTypeMismatch         = errmodel.E(CodeTypeMismatch)
	ErrUntrustedIssuer      = errmodel.E(CodeUntrustedIssuer)
	ErrUnauthorized         = errmodel.E(CodeUnauthorized)
	ErrNotFound             = errmodel.E(CodeNotFound)
	ErrFetch                = errmodel.E(CodeFetch)
	ErrRedirect             = errmodel.E(CodeRedirect)
	ErrDigestCollision      = errmodel.E(CodeDigestCollision)
)

Functions

func Digest

func Digest(payload []byte) string

func NewPublisher

func NewPublisher(lookup LookupDocument, authorize AuthorizeRequest) http.Handler

NewPublisher returns the framework-neutral well-known publication handler.

func NormalizeReferences

func NormalizeReferences(in map[string]string) (map[string]string, error)

NormalizeReferences prepares a mint-time documents claim. It trims types, canonicalizes digests, and rejects normalization collisions.

func NormalizeType

func NormalizeType(value string) (string, error)

func ParseReferencesJSON

func ParseReferencesJSON(raw []byte) (map[string]string, error)

ParseReferencesJSON strictly parses a documents claim. Duplicate keys and non-canonical values are rejected instead of being overwritten by map decode.

func ValidateDigest

func ValidateDigest(value string) error

func ValidateType

func ValidateType(value string) error

ValidateType requires a bounded opaque identifier ending in /vN. AuthKit does not interpret the namespace or version beyond enforcing that the version is present in the type itself.

Types

type AuthorizeRequest

type AuthorizeRequest func(*http.Request) error

AuthorizeRequest either authenticates an incoming publisher request or adds existing machine credentials to an outgoing resolver request. Nil always denies; AuthKit does not define a document-specific credential.

type DocumentVerifier

type DocumentVerifier interface {
	ValidateDocumentIssuer(context.Context, string) error
	VerifyDocument(context.Context, SignedDocument, VerifyOptions) (Envelope, error)
}

DocumentVerifier is implemented by verify.Verifier. The preflight trust check prevents an untrusted issuer from becoming a network destination.

type Envelope

type Envelope struct {
	Issuer    string          `json:"iss"`
	Audiences []string        `json:"aud"`
	Type      string          `json:"type"`
	Payload   json.RawMessage `json:"payload"`
}

Envelope is the signed JWS payload. Payload is intentionally opaque to AuthKit and may contain any valid JSON value.

func DecodeEnvelope

func DecodeEnvelope(payload []byte) (Envelope, error)

DecodeEnvelope strictly decodes the signed payload, rejecting duplicate or unknown fields before any application payload can be returned.

func (Envelope) HasAudience

func (e Envelope) HasAudience(audience string) bool

func (Envelope) Validate

func (e Envelope) Validate() error
type Header struct {
	Algorithm string
	KeyID     string
	Type      string
}

Header is the security-relevant subset of an inspected compact JWS header. Inspecting a header or payload does not verify its signature.

func DecodeCompact

func DecodeCompact(compact string) (Header, []byte, error)

DecodeCompact strictly inspects a compact JWS and returns its exact decoded payload. It does not verify the signature.

type LookupDocument

type LookupDocument func(context.Context, string) (SignedDocument, error)

LookupDocument returns a retained document by its immutable payload digest. Its compact JWS representation may change when the payload is re-signed.

type Reference

type Reference struct {
	Type   string `json:"type"`
	Digest string `json:"digest"`
}

Reference identifies one exact signed envelope. Type carries the application schema version (for example, example.catalog/v1); Digest covers the exact JWS payload bytes, not a decoded/re-encoded JSON value.

func ReferenceFor

func ReferenceFor(documentType string, signedPayload []byte) (Reference, error)

func (Reference) Validate

func (r Reference) Validate() error

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}
Example (TwoSites)
package main

import (
	"context"
	"crypto"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/open-rails/authkit/documents"
	"github.com/open-rails/authkit/jwtkit"
	"github.com/open-rails/authkit/verify"
)

const machineAuthorization = "Bearer existing-machine-credential"

func requireMachine(request *http.Request) error {
	if request.Header.Get("Authorization") != machineAuthorization {
		return errors.New("unauthorized")
	}
	return nil
}

func addMachine(request *http.Request) error {
	request.Header.Set("Authorization", machineAuthorization)
	return nil
}

func main() {
	signer, _ := jwtkit.NewRSASigner(2048, "site-a-key")
	var document documents.SignedDocument
	siteA := httptest.NewServer(documents.NewPublisher(func(context.Context, string) (documents.SignedDocument, error) {
		return document, nil
	}, requireMachine))
	defer siteA.Close()
	document, _ = documents.Sign(context.Background(), signer, documents.Envelope{
		Issuer: siteA.URL, Audiences: []string{"site-b"}, Type: "example.entitlements/v1", Payload: json.RawMessage(`{"plan":"starter"}`),
	})

	v := verify.NewVerifier()
	_ = v.AddIssuer(siteA.URL, nil, verify.IssuerOptions{RawKeys: map[string]crypto.PublicKey{signer.KID(): signer.PublicKey()}})
	resolver := documents.NewResolver(v, siteA.Client(), addMachine, documents.ResolverOptions{AllowHTTP: true})
	siteB := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		payload, err := resolver.Resolve(r.Context(), siteA.URL, document.Reference, "site-b")
		if err != nil {
			http.Error(w, err.Error(), http.StatusUnauthorized)
			return
		}
		_, _ = w.Write(payload) // site B owns schema/processing from here.
	}))
	defer siteB.Close()

	response, _ := siteB.Client().Get(siteB.URL)
	body, _ := io.ReadAll(response.Body)
	response.Body.Close()
	fmt.Println(string(body))
}
Output:
{"plan":"starter"}

func NewResolver

func NewResolver(verifier DocumentVerifier, client *http.Client, authorize AuthorizeRequest, opts ResolverOptions) *Resolver

func (*Resolver) Resolve

func (r *Resolver) Resolve(ctx context.Context, issuer string, reference Reference, audience string) (json.RawMessage, error)

Resolve authenticates, fetches, verifies, and returns only the opaque application payload. Successful results are cached by issuer+type+digest.

type ResolverOptions

type ResolverOptions struct {
	// AllowHTTP is for local tests and development only. Production defaults to
	// HTTPS-only publication endpoints.
	AllowHTTP        bool
	Timeout          time.Duration
	MaxResponseBytes int64
	MaxCacheEntries  int
}

type Service added in v0.91.0

type Service struct {
	// contains filtered or unexported fields
}

Service owns the publish lifecycle of ONE immutable signed document: sign -> verify -> persist -> re-read -> re-verify at construction, a digest-stable re-signature when the signing key rotates (EnsureSigningKID / CurrentDigest repair), and content-addressed lookups for the publication route. Construct with NewService; the zero value is unusable.

func NewService added in v0.91.0

func NewService(ctx context.Context, cfg ServiceConfig) (*Service, error)

NewService validates the config and runs the full publish lifecycle: the document is signed, self-verified, persisted, re-read, and re-verified before the Service (and therefore its digest) becomes observable.

func (*Service) CurrentDigest added in v0.91.0

func (s *Service) CurrentDigest(ctx context.Context) (string, error)

CurrentDigest re-validates the persisted artifact behind the process snapshot and returns its digest. An artifact that fails signature verification (e.g. it was re-signed by a key this process no longer serves) is repaired in place by a fresh digest-stable re-signature.

func (*Service) EnsureSigningKID added in v0.91.0

func (s *Service) EnsureSigningKID(ctx context.Context, tokenKID string) error

EnsureSigningKID reconciles the persisted artifact's signature with the key that just signed a token stamping this document's digest (ak#261): when the stored compact JWS is not signed by tokenKID, the document is re-signed — digest-stable — so a verifier holding the token's JWKS can always verify the document it references.

func (*Service) Lookup added in v0.91.0

func (s *Service) Lookup(ctx context.Context, digest string) (SignedDocument, error)

Lookup returns any persisted document by digest (the publication route's lookup seam) — historical digests included, not just the process snapshot.

func (*Service) Payload added in v0.91.0

func (s *Service) Payload() json.RawMessage

Payload returns a copy of the host payload this Service published.

func (*Service) Reference added in v0.91.0

func (s *Service) Reference() Reference

Reference returns the process snapshot reference of the published document.

type ServiceConfig added in v0.91.0

type ServiceConfig struct {
	// Type is the versioned application document type (e.g. "example.catalog/v1").
	Type string
	// Payload is the host-compiled application payload. Opaque to AuthKit.
	Payload json.RawMessage
	// Issuer is the signing AuthKit issuer (normally Config.Token.Issuer).
	Issuer string
	// Audiences are the signed envelope audiences readers verify against.
	Audiences []string
	// Signer signs and self-verifies (normally the *embedded.Client).
	Signer Signer
	// Store persists the document (normally embedded.Client.DocumentStore()).
	Store Store
}

ServiceConfig declares one published document (ak#260). The host supplies its compiled payload and the engine's store; AuthKit owns the lifecycle and invariants.

type SignedDocument

type SignedDocument struct {
	CompactJWS    string    `json:"jws"`
	Reference     Reference `json:"reference"`
	SignedPayload []byte    `json:"signed_payload"`
}

SignedDocument retains both the compact JWS and the exact bytes used as its payload so callers can publish them without a parse/re-encode step.

func FromCompact

func FromCompact(compact string, reference Reference) (SignedDocument, error)

func Sign

func Sign(ctx context.Context, signer jwtkit.Signer, envelope Envelope) (SignedDocument, error)

Sign marshals the normalized envelope once, signs those exact bytes, and returns the retained bytes and their content-addressed reference.

type Signer added in v0.91.0

type Signer interface {
	SignDocument(ctx context.Context, envelope Envelope) (SignedDocument, error)
	PublicKeysByKID() map[string]crypto.PublicKey
}

Signer signs one envelope with the process's active AuthKit key and exposes the CURRENT public keys for self-verification. *embedded.Client satisfies it.

type Store added in v0.91.0

type Store interface {
	SaveDocument(ctx context.Context, document SignedDocument) error
	Lookup(ctx context.Context, digest string) (SignedDocument, error)
}

Store persists immutable signed documents by digest. Save may replace ONLY compact_jws for an existing digest (a re-signature of the same payload on key rotation); any payload or type change under an existing digest must fail with ErrDigestCollision. Lookup returns ErrNotFound for unknown digests.

type VerifyOptions

type VerifyOptions struct {
	Issuer    string
	Audience  string
	Type      string
	Reference Reference
}

VerifyOptions are the caller's authenticated expectations. None may be inferred from unsigned request metadata.

Jump to

Keyboard shortcuts

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