Documentation
¶
Overview ¶
Package signer is the CLI-side RFC 9421 signing surface. It is deliberately distinct from chassis/auth/signature/ — that package wraps yaronf/httpsign for both signing and verifying, and httpsign requires a concrete ed25519.PrivateKey at construction time. We need to plug in alternative backends (ssh-agent today, hardware tokens later), so the outbound side owns its own canonicalization here.
The chassis-side verifier (chassis/auth/signature/verifier.go) does NOT use anything from this package — it keeps using httpsign. The only contract between us and the server is the wire format: same covered components, same Signature-Input parameters, same Ed25519 signature bytes.
Index ¶
- Variables
- func Fingerprint(pub ed25519.PublicKey) string
- func LoadEd25519PrivateKey(path string, promptPassphrase bool) (ed25519.PrivateKey, error)
- func LoadEd25519PrivateKeyWithPassphrase(path string, passphrase []byte) (ed25519.PrivateKey, error)
- type AgentSigner
- type FileKeySigner
- type PassphraseMissingError
- type Signer
Constants ¶
This section is empty.
Variables ¶
var ErrKeyNotInAgent = errors.New("expected key not present in ssh-agent")
ErrKeyNotInAgent signals "agent is reachable but doesn't hold a key matching the one we expect." Most commonly: the user logged out / ssh-add -D'd between enrollment and the next signed call.
var ErrNoAgent = errors.New("ssh-agent not available (SSH_AUTH_SOCK unset or unreachable)")
ErrNoAgent signals "$SSH_AUTH_SOCK is unset or the socket can't be reached." Surface this as a typed sentinel so the enrollment resolver can distinguish "no agent" from "agent reachable but has no matching key" — the two cases need different operator messages.
Functions ¶
func Fingerprint ¶
Fingerprint returns the SHA256 fingerprint of an Ed25519 public key in the canonical ssh-keygen `-lf` format (`SHA256:<base64-no-pad>`). Computes over the SSH wire format, not the raw 32 bytes, so the value matches `ssh-add -l` / `ssh-keygen -lf` output verbatim — letting users cross-reference txco's choice against what their ssh tooling shows.
Falls back to a raw-key SHA256 (still useful as a stable identifier) only if ssh.NewPublicKey somehow fails, which can't happen for a well-formed 32-byte ed25519 key but is handled defensively.
func LoadEd25519PrivateKey ¶
func LoadEd25519PrivateKey(path string, promptPassphrase bool) (ed25519.PrivateKey, error)
LoadEd25519PrivateKey reads path and decodes it to a raw ed25519.PrivateKey. Handles PKCS#8 PEM, OpenSSH PEM, and encrypted OpenSSH (with passphrase prompt when promptPassphrase is true and stdin is a TTY).
Encrypted PKCS#8 is intentionally not supported — too rare to be worth the surface; users get a clear "re-encrypt with `ssh-keygen -p`" message via the underlying ssh.PassphraseMissingError path.
func LoadEd25519PrivateKeyWithPassphrase ¶
func LoadEd25519PrivateKeyWithPassphrase(path string, passphrase []byte) (ed25519.PrivateKey, error)
LoadEd25519PrivateKeyWithPassphrase decodes an encrypted PEM key using the supplied passphrase. Returns a clear error if path is not an encrypted key or the passphrase is wrong.
Types ¶
type AgentSigner ¶
type AgentSigner struct {
// contains filtered or unexported fields
}
AgentSigner signs via an ssh-agent. The private key never leaves the agent's process (or the hardware token it brokers — Yubikeys, Secure Enclave via Secretive, PKCS#11 cards). We only ever send the agent the canonical RFC 9421 signature base; the agent returns a raw Ed25519 signature blob which we set verbatim in the Signature header.
func NewAgentSigner ¶
func NewAgentSigner(keyID string, wantPub ed25519.PublicKey) (*AgentSigner, error)
NewAgentSigner dials $SSH_AUTH_SOCK and locates wantPub. Returns typed sentinels for the two failure modes that need distinct CLI messages: ErrNoAgent (agent unreachable) and ErrKeyNotInAgent (agent reachable, doesn't have wantPub).
func NewAgentSignerFromAgent ¶
func NewAgentSignerFromAgent(keyID string, wantPub ed25519.PublicKey, a agent.Agent) (*AgentSigner, error)
NewAgentSignerFromAgent is the testable constructor; production callers use NewAgentSigner. Accepts any agent.Agent so unit tests can plug in agent.NewKeyring() pre-loaded with the expected key.
func (*AgentSigner) PublicKey ¶
func (s *AgentSigner) PublicKey() ed25519.PublicKey
PublicKey implements Signer.
func (*AgentSigner) Sign ¶
func (s *AgentSigner) Sign(req *http.Request, body []byte) error
Sign implements Signer. Same canonical base as FileKeySigner; the signing primitive is `agent.Agent.Sign` instead of `ed25519.Sign`. We strictly validate the returned signature's Format and Blob length before trusting it.
type FileKeySigner ¶
type FileKeySigner struct {
// contains filtered or unexported fields
}
FileKeySigner signs RFC 9421 requests with an Ed25519 private key loaded from a PEM file. The reader transparently handles both the legacy PKCS#8 form txco shipped originally AND the OpenSSH form `ssh-keygen` produces, via crypto/ssh.ParseRawPrivateKey. Existing developer keys keep working; new keys can come straight from a standard SSH workflow.
func NewFileKeySigner ¶
func NewFileKeySigner(keyID, path string, promptPassphrase bool) (*FileKeySigner, error)
NewFileKeySigner loads path and returns a signer. Encrypted-key behavior depends on promptPassphrase:
- true + TTY: prompts the user via term.ReadPassword.
- true + non-TTY (pipe): returns *PassphraseMissingError so the CLI can surface a clear "this key is encrypted; pass it via --passphrase or run interactively" error.
- false: never prompts; *PassphraseMissingError straight away.
func NewFileKeySignerFromKey ¶
func NewFileKeySignerFromKey(keyID string, priv ed25519.PrivateKey) (*FileKeySigner, error)
NewFileKeySignerFromKey constructs a signer from an already-loaded private key. Useful for tests and for the enrollment path that generates a fresh key in-memory before persisting it.
func (*FileKeySigner) PublicKey ¶
func (s *FileKeySigner) PublicKey() ed25519.PublicKey
PublicKey implements Signer.
type PassphraseMissingError ¶
type PassphraseMissingError struct{ Path string }
PassphraseMissingError is the typed sentinel callers use to detect "this key is encrypted, you need to supply a passphrase." Mirrors ssh.PassphraseMissingError but is owned by this package so callers don't have to import x/crypto/ssh just for type assertions.
func (*PassphraseMissingError) Error ¶
func (e *PassphraseMissingError) Error() string
Error implements error.
type Signer ¶
type Signer interface {
// KeyID is the RFC 9421 keyid value the chassis registry uses
// to look up the public key for verification. Always non-empty
// for a usable Signer.
KeyID() string
// PublicKey is the raw 32-byte Ed25519 public key for this
// signer, useful for fingerprint display and meta cross-checks.
PublicKey() ed25519.PublicKey
// Sign sets the three required headers on req. body is the
// already-marshaled request body (nil for GETs / empty bodies).
// Implementations must:
// 1. compute Content-Digest from body and set the header,
// 2. compute the RFC 9421 signature base for the covered
// components + chosen params,
// 3. sign the base bytes via the backend (raw ed25519, agent, …),
// 4. set Signature-Input + Signature headers.
Sign(req *http.Request, body []byte) error
}
Signer signs an outgoing *http.Request in place. After Sign returns, the request carries Content-Digest, Signature-Input, and Signature headers in the same wire format the chassis verifier expects — regardless of whether the signing key lives in process memory, in an ssh-agent, or on a hardware token.