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 ¶
- Constants
- Variables
- func Digest(payload []byte) string
- func NewPublisher(lookup LookupDocument, authorize AuthorizeRequest) http.Handler
- func NormalizeDigest(value string) (string, error)
- func NormalizeReferences(in map[string]string) (map[string]string, error)
- func NormalizeType(value string) (string, error)
- func ParseReferencesJSON(raw []byte) (map[string]string, error)
- func ValidateDigest(value string) error
- func ValidateReferences(references map[string]string) error
- func ValidateType(value string) error
- type AuthorizeRequest
- type DocumentVerifier
- type Envelope
- type Header
- type LookupDocument
- type Reference
- type Resolver
- type ResolverOptions
- type Service
- func (s *Service) CurrentDigest(ctx context.Context) (string, error)
- func (s *Service) EnsureSigningKID(ctx context.Context, tokenKID string) error
- func (s *Service) Lookup(ctx context.Context, digest string) (SignedDocument, error)
- func (s *Service) Payload() json.RawMessage
- func (s *Service) Reference() Reference
- type ServiceConfig
- type SignedDocument
- type Signer
- type Store
- type VerifyOptions
Examples ¶
Constants ¶
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 )
const PublicationPathPrefix = "/.well-known/authkit/documents/"
Variables ¶
var ( ErrInvalidReference = errors.New("invalid_document_reference") ErrInvalidType = errors.New("invalid_document_type") ErrInvalidDigest = errors.New("invalid_document_digest") ErrDuplicateReference = errors.New("duplicate_document_reference") ErrTooManyReferences = errors.New("too_many_document_references") ErrReferencesTooLarge = errors.New("document_references_too_large") ErrWrongTokenType = errors.New("documents_wrong_token_type") ErrReservedAttribute = errors.New("reserved_document_attribute") ErrInvalidEnvelope = errors.New("invalid_document_envelope") ErrPayloadTooLarge = errors.New("document_payload_too_large") ErrMalformedJWS = errors.New("malformed_document_jws") ErrWrongJOSEType = errors.New("wrong_document_jose_type") ErrUnsupportedAlgorithm = errors.New("unsupported_document_algorithm") ErrUnsupportedSigner = errors.New("unsupported_document_signer") ErrUnknownKey = errors.New("unknown_document_key") ErrInvalidSignature = errors.New("invalid_document_signature") ErrDigestMismatch = errors.New("document_digest_mismatch") ErrIssuerMismatch = errors.New("document_issuer_mismatch") ErrAudienceMismatch = errors.New("document_audience_mismatch") ErrTypeMismatch = errors.New("document_type_mismatch") ErrUntrustedIssuer = errors.New("untrusted_document_issuer") ErrNotFound = errors.New("document_not_found") ErrFetch = errors.New("document_fetch_failed") ErrRedirect = errors.New("document_redirect_rejected") )
var ErrDigestCollision = errors.New("document_digest_collision")
ErrDigestCollision is returned when a save would change the immutable payload or type stored under an existing digest.
Functions ¶
func NewPublisher ¶
func NewPublisher(lookup LookupDocument, authorize AuthorizeRequest) http.Handler
NewPublisher returns the framework-neutral well-known publication handler.
func NormalizeDigest ¶
func NormalizeReferences ¶
NormalizeReferences prepares a mint-time documents claim. It trims types, canonicalizes digests, and rejects normalization collisions.
func NormalizeType ¶
func ParseReferencesJSON ¶
ParseReferencesJSON strictly parses a documents claim. Duplicate keys and non-canonical values are rejected instead of being overwritten by map decode.
func ValidateDigest ¶
func ValidateReferences ¶
func ValidateType ¶
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 ¶
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 ¶
DecodeEnvelope strictly decodes the signed payload, rejecting duplicate or unknown fields before any application payload can be returned.
func NormalizeEnvelope ¶
NormalizeEnvelope returns a detached, normalized copy suitable for one-time marshaling and signing.
func (Envelope) HasAudience ¶
type Header ¶
Header is the security-relevant subset of an inspected compact JWS header. Inspecting a header or payload does not verify its 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 ¶
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 NormalizeReference ¶
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
type ResolverOptions ¶
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
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
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
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.
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
// Postgres + Schema select AuthKit's own signed_documents table (Schema ""
// means the default "profiles"). Ignored when Store is set.
Postgres *pgxpool.Pool
Schema string
// Store overrides the built-in Postgres store (tests / custom backends).
Store Store
}
ServiceConfig declares one published document (ak#260). The host supplies its compiled payload; AuthKit owns the store, 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)
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.