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 claim-based tokens (JWT/PASETO): it does not encode issuers or arbitrary claims. Instead, it provides a compact token that binds a key name, 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 name) 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 single signing key used for Generate.
- Config.Keys is a set of named public keys used for Verify.
- Config.Expiration controls how long generated tokens remain valid.
Verification is “name-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 name exists, verification fails.
This design supports key rotation and multi-key verification: you can mint tokens with the active signing key name 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 in Key.
Those configs commonly support go-service “source strings” for key sources (for example env:/file:/literal). 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 key name on success (the kid field from the signed claims). On failure, it returns an empty name plus an error. Common failure modes include:
- token does not contain the "." separator,
- no verification key exists for the extracted name,
- 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.
Some invalid-token cases are intentionally collapsed into a generic “invalid match” class so callers do not learn whether a name exists or which specific check failed. 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 name, audience, and validity window. It does not provide nonce/jti replay protection for repeated calls to the same audience inside the validity window. If your use case requires one-time-use tokens, prefer JWT or PASETO token kinds with jti tracking or layer additional checks at a higher level.
Relationship to the top-level token facade ¶
Services often use the top-level 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 {
// Key is the signing key configuration used to mint SSH-style tokens.
//
// This should include the private key material (via the embedded crypto/ssh.Config
// fields) and a logical Name that will be embedded in minted tokens.
//
// If Key is nil, Token.Generate will not be usable.
Key *Key `yaml:"key,omitempty" json:"key,omitempty" toml:"key,omitempty"`
// Keys is the set of verification keys that may be used to validate SSH-style tokens.
//
// Verification uses the token's embedded key id to select a key from this set. If Keys
// is empty or does not contain the token's key id, verification fails.
//
// If Keys is nil/empty, Token.Verify will not be usable.
Keys Keys `yaml:"keys,omitempty" json:"keys,omitempty" toml:"keys,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".
Expiration time.Duration `yaml:"exp,omitempty" json:"exp,omitempty" toml:"exp,omitempty" validate:"gt=0"`
}
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 single signing key used by Token.Generate.
- Keys is the set of verification keys that Token.Verify may use.
- Expiration is how long newly generated tokens are valid.
Key rotation and multi-key verification ¶
Verification is name-based: the signed token claims embed a key id, and verification selects a matching public key config from Keys (via Keys.Get(name)). This design supports key rotation by allowing you to:
- mint new tokens with the active signing key name, and
- continue verifying older tokens by keeping historical public keys in Keys.
Note: This package does not enforce that Key.Name exists in Keys. If you want tokens minted by Key to be verifiable by this same Config, include the corresponding public key entry in Keys under the same name.
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"`
// Name is the logical key name used to select a key (for example via Keys.Get).
//
// For signing, this name is embedded into the signed token claims as the key id.
// For verification, this name is used as the lookup key into Keys.
Name string `yaml:"name,omitempty" json:"name,omitempty" toml:"name,omitempty"`
}
Key describes SSH key material configuration along with its logical name.
The embedded crypto/ssh.Config provides the public/private key source configuration used by go-service crypto/ssh helpers (typically via an os.FS).
The Name identifies the key logically and is used to select keys during verification.
type Keys ¶
type Keys []*Key
Keys is a list of named SSH keys.
This is used for verification key selection. Names are expected (but not required) to be unique; Get returns the first match.
type Signer ¶ added in v2.9.0
Signer is an alias for crypto/ssh.Signer.
It represents an object 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 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.Name
- 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 identify the signing key instead of carrying a subject.
The signature is produced by signing the exact JSON claims bytes using the configured signing key material. Because the key name and audience are both in the signed claims, a token minted for one audience cannot be replayed for another audience.
High-level algorithm:
- Load an SSH signer from the configured signing key (t.cfg.Key) using fs.
- Marshal claims = {"ver": "v1", "kid": <name>, "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 key name 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 name, 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.
- Base64 decode errors and verifier loading errors are returned as-is.
On success, Verify returns the extracted key name. On failure, it always returns an empty name alongside the error.