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 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 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 ( 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.") 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.
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) ErrNotFound = errmodel.E(CodeNotFound) ErrFetch = errmodel.E(CodeFetch) ErrRedirect = errmodel.E(CodeRedirect) ErrDigestCollision = errmodel.E(CodeDigestCollision) )
Functions ¶
func NewPublisher ¶
func NewPublisher(lookup LookupDocument, authorize AuthorizeRequest) http.Handler
NewPublisher returns the framework-neutral well-known publication handler.
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 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 (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.
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
// 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)
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.