signer

package
v0.2.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 8, 2026 License: MPL-2.0 Imports: 16 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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

func Fingerprint(pub ed25519.PublicKey) string

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) KeyID

func (s *AgentSigner) KeyID() string

KeyID implements Signer.

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) KeyID

func (s *FileKeySigner) KeyID() string

KeyID implements Signer.

func (*FileKeySigner) PublicKey

func (s *FileKeySigner) PublicKey() ed25519.PublicKey

PublicKey implements Signer.

func (*FileKeySigner) Sign

func (s *FileKeySigner) Sign(req *http.Request, body []byte) error

Sign implements Signer. Walks the standard canonicalize → sign → set-headers path with raw `ed25519.Sign`. Same wire output as the AgentSigner; only the signing primitive differs.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL