Documentation
¶
Overview ¶
Package ssh provides an SSH-style token format for go-service.
This package implements a simple signed token scheme using SSH public key cryptography. It is intentionally different from issuer-based tokens (JWT/PASETO): it does not encode issuers or arbitrary claims. Instead, it provides a compact token that binds a key id, subject, audience, issued-at time, and expiration claims to a signature.
Token format ¶
Tokens are ASCII strings of the form:
<base64(json-claims)>.<base64(signature)>
Where:
- json-claims contains "kid" (the logical signing key id), "sub" (the subject, which must equal "kid"), and "aud" (the expected audience, such as an HTTP path or gRPC method), plus "iat" and "exp" Unix nanosecond timestamps.
- signature is produced by signing the exact JSON claims bytes with the configured SSH private key.
- base64(signature) is the standard base64 encoding of the raw signature bytes.
Signing keys and verification keys ¶
Configuration is provided via Config:
- Config.Key is the active signing key id used for Generate.
- Config.Keys is a named key set used for Generate and Verify.
- Config.Expiration controls how long generated tokens remain valid.
- Config.Leeway optionally tolerates small verifier/issuer clock skew around iat and exp while preserving the signed lifetime cap.
Verification is id-based: Verify extracts kid from the signed claims and then looks up a matching public key configuration in Config.Keys (via Keys.Get(kid)). If no key with that id exists, verification fails. Verify also requires "sub" to match "kid" and returns that subject on success.
This design supports key rotation and multi-key verification: you can mint tokens with the active signing key id while allowing verification against multiple historical/active public keys by including them in Config.Keys.
Key material loading and "source strings" ¶
The Token constructor accepts an *os.FS and uses go-service crypto/ssh helpers to load key material based on the embedded crypto/ssh.Config selected from Keys.
Those configs commonly support go-service "source strings" for key sources (for example "env:SSH_KEY", "file:<path>", or a literal value). File paths may be absolute or relative according to os.FS.ReadSource. Resolution and filesystem behavior depend on the go-service os.FS and crypto/ssh packages used by your wiring.
Error handling expectations ¶
Verify returns the subject on success. For SSH tokens this subject must match the kid field from the signed claims. On failure, it returns an empty subject plus an error. Common failure modes include:
- token does not contain the "." separator,
- no verification key exists for the extracted id,
- the signed audience does not match the expected audience,
- the token is expired or not yet valid,
- base64 decoding fails,
- signature verification fails,
- key material cannot be loaded.
Structural mismatches, unknown key ids, version mismatches, subject/key-id mismatches, and signature mismatches are intentionally collapsed into a generic "invalid match" class so callers do not learn whether a name exists or which structural check failed. Audience mismatches return token/errors.ErrInvalidAudience, and time-window or signed-lifetime failures return token/errors.ErrInvalidTime. Callers that need fine-grained diagnostics should add logging/metrics at the call site rather than relying on error text.
Security notes ¶
This scheme authenticates possession of a key (via signature verification) and binds that to a logical subject/key id, audience, and validity window. It does not provide replay protection for repeated calls to the same audience inside the validity window. JWT and PASETO tokens include generated jti values that callers can store and check, but this repository does not maintain replay state for any token kind. If your use case requires one-time-use tokens, add durable caller-owned replay tracking at a higher level.
Relationship to the top-level token facade ¶
Services often use the top-level github.com/alexfalkowski/go-service/v2/token.Token facade (package token), which delegates to this implementation when [Config.Kind] == "ssh".
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Keys is the named key set used to mint and verify SSH-style tokens.
//
// Generation uses Key to select private key material. Verification uses the
// token's embedded key id to select public key material.
//
// If Keys is nil/empty, Token.Verify will not be usable.
Keys Keys `yaml:"keys,omitempty" json:"keys,omitempty" toml:"keys,omitempty"`
// Key is the active signing key id used to mint SSH-style tokens.
//
// The selected key id is embedded in minted tokens as both the "kid" and "sub"
// claims because SSH tokens authenticate the trusted peer key itself.
Key string `yaml:"key,omitempty" json:"key,omitempty" toml:"key,omitempty"`
// Expiration is the duration used to set token expiration.
//
// In config files it is encoded as a Go duration string, for example "15m" or "1h".
// It must use whole-second precision.
Expiration time.Duration `yaml:"exp,omitempty" json:"exp,omitempty" toml:"exp,omitempty" validate:"duration_second_precision"`
// Leeway is the optional clock-skew tolerance applied during verification.
//
// A zero value keeps strict time validation. Non-zero values allow iat to be
// slightly in the future and exp to be slightly in the past while preserving
// the signed lifetime cap enforced from iat to exp.
Leeway time.Duration `yaml:"leeway,omitempty" json:"leeway,omitempty" toml:"leeway,omitempty" validate:"omitempty,duration_second_precision"`
}
Config configures the SSH-style token implementation.
This token kind uses a simple signed token format (see package ssh docs) and requires SSH key material for:
- signing (minting tokens), and
- verification (validating tokens).
Config separates those concerns:
- Key is the active signing key id used by Token.Generate.
- Keys is the named key set that Token.Generate and Token.Verify may use.
- Expiration is how long newly generated tokens are valid.
Key rotation and multi-key verification ¶
Verification is id-based: the signed token claims embed a key id, and verification selects a matching public key config from Keys (via Keys.Get). This design supports key rotation by allowing you to:
- mint new tokens with the active signing key id, and
- continue verifying older tokens by keeping historical public keys in Keys.
The active Key entry must exist in Keys and include private key material when generating tokens.
Enablement ¶
Enablement is modeled by presence and content: a nil *Config is disabled, and a config with neither Key nor Keys is disabled (see IsEnabled).
type Key ¶
type Key struct {
// Config contains the SSH key material configuration (public/private key sources).
*ssh.Config `yaml:",inline" json:",inline" toml:",inline"`
// contains filtered or unexported fields
}
Key describes SSH key material configuration.
The embedded github.com/alexfalkowski/go-service/v2/crypto/ssh.Config provides the public/private key source configuration used by go-service crypto/ssh helpers (typically via an os.FS).
func (*Key) Signer ¶ added in v2.675.0
Signer loads an SSH signer for k.
The signer is resolved at most once per process lifetime and reused for subsequent calls. Only a successful load is cached, so a transient read or parse failure is retried on the next call rather than being cached forever.
It returns github.com/alexfalkowski/go-service/v2/token/errors.ErrInvalidConfig when k is nil, its embedded key config is nil, or fs is nil.
func (*Key) Verifier ¶ added in v2.675.0
Verifier loads an SSH verifier for k.
The verifier is resolved at most once per process lifetime and reused for subsequent calls. Only a successful load is cached, so a transient read or parse failure is retried on the next call rather than being cached forever.
It returns github.com/alexfalkowski/go-service/v2/token/errors.ErrInvalidConfig when k is nil, its embedded key config is nil, or fs is nil.
type Keys ¶
Keys maps key ids to SSH key material.
This is used for signing and verification key selection.
type Signer ¶ added in v2.9.0
Signer is an alias for github.com/alexfalkowski/go-service/v2/crypto/ssh.Signer.
It represents a go-service SSH signer capable of producing signatures using SSH key material.
type Token ¶
type Token struct {
// contains filtered or unexported fields
}
Token generates and verifies SSH-style tokens.
This token kind is intentionally simple. It binds a logical key name and audience to a signature.
Missing per-operation key material is treated as invalid configuration and reported via github.com/alexfalkowski/go-service/v2/token/errors.ErrInvalidConfig.
func NewToken ¶
NewToken constructs a Token using cfg and fs.
The returned Token loads key material using fs when generating and verifying tokens.
Enablement is modeled by configuration: if cfg is disabled (see Config.IsEnabled), NewToken returns nil.
func (*Token) Generate ¶
Generate creates an SSH-style token for the given audience.
Token format:
<base64(json-claims)>.<base64(signature)>
Where json-claims contains:
- ver: the token format version ("v1")
- kid: t.cfg.Key
- sub: t.cfg.Key
- aud: aud
- iat: the issued-at Unix nanosecond timestamp
- exp: the expiration Unix nanosecond timestamp
The sub parameter is accepted to match the other token implementations; SSH tokens authenticate the active peer key, so the subject is the active key id.
The signature is produced by signing the exact JSON claims bytes using the configured signing key material. Because the key id and audience are both in the signed claims, a token minted for one audience cannot be replayed for another audience.
High-level algorithm:
- Look up the active key config from t.cfg.Keys using t.cfg.Key.
- Load an SSH signer from that key config using fs.
- Marshal claims = {"ver": "v1", "kid": <key>, "sub": <key>, "aud": <aud>, "iat": <now>, "exp": <expiration>}.
- Compute signature = Sign(claims).
- Return "<base64(claims)>.<base64(signature)>".
Errors are returned when the signing key configuration is missing/partial, key material cannot be loaded, claims encoding fails, or signature generation fails.
func (*Token) Verify ¶
Verify validates token for aud and returns the embedded subject if it is valid.
Token format:
<base64(json-claims)>.<base64(signature)>
Verification is name-based and audience-bound: Verify decodes the signed claims, checks claims.aud against aud, selects the matching verification key configuration from t.cfg.Keys using Keys.Get(claims.kid), and verifies the signature over the exact claims bytes with that key.
High-level algorithm:
- Split token into (encodedClaims, encodedSignature) on ".".
- Decode and unmarshal the claims.
- Look up a verification key config for claims.kid in t.cfg.Keys.
- Load an SSH verifier from the selected key material using fs.
- Decode the signature from base64.
- Verify(signature, claims).
- Check claims.ver, claims.aud, claims.iat, and claims.exp.
Security-oriented error behavior:
- If the token cannot be split, the claims cannot be decoded, or no key exists for the id, Verify returns crypto/errors.ErrInvalidMatch. This intentionally collapses multiple invalid-token cases into a single class to avoid leaking whether a given key name exists.
- If a matching key name exists but its verification config is missing/partial, Verify returns token/errors.ErrInvalidConfig.
- Claim base64 decode errors are collapsed to crypto/errors.ErrInvalidMatch. Signature base64 decode errors and verifier loading errors are returned as-is.
On success, Verify returns the extracted subject. For SSH tokens this subject must match the signed key id. On failure, it always returns an empty subject alongside the error.
type Verifier ¶ added in v2.9.0
Verifier is an alias for github.com/alexfalkowski/go-service/v2/crypto/ssh.Verifier.
It represents a go-service SSH verifier capable of verifying signatures produced by an SSH signer.