-
Notifications
You must be signed in to change notification settings - Fork 83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Plugins system for authentication #135
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code looks great, looking forward to using the plugin to add OpenPubkey. I did a small code review to help myself learn the code.
if err == nil { | ||
pubkey = filePubkey | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to debug log the err pubkeyBytes, err := os.ReadFile(fmt.Sprintf("%s.pub", m.Filename()))
in case the error isn't just "file not found"? Maybe someone messed up the permissions and made it write-only or the system ran out of file descriptors?
In the case of ssh.ParseAuthorizedKey
this means we found a matching .pub file and the file is somehow wrong. That probably means the user did something wrong in creating it. Might save a user sometime if you print a warning to the console.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, thanks ! I think I'll put it at Debug level though since it is happening in an optional mechanism searching for the .pub
counterpart of the key, which may simply be a different file for whatever reason.
} | ||
} | ||
// now, try to see of the agent manages this key | ||
foundAgentKey := false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
foundAgentKey
is always false, if !foundAgentKey {
is always true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch, it's an artefact from the non-plugin behaviour.
In fact we can just remove foundAgentKey
.
return "RS256" | ||
case "ssh-ed25519": | ||
return "EdDSA" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging an error here, or returning an ok value, or catching the returned "" could be valuable as it looks like the server supports "RS256", "EdDSA", "ES256" but the client over supports "RS256", "EdDSA". Alternatively adding "ES256" to the client Alg()
I haven't tested this but reading the code it seems like if "ES256" is used on the client Alg()
will return "", which will cause the server to throw "unsupported signature algorithm" in server_plugin.go. This could be confusing because it is the client which doesn't support "ES256".
ssh3/auth/plugins/pubkey_authentication/server/server_plugin.go
Lines 40 to 44 in 92aeb1b
switch unvalidatedToken.Method.Alg() { | |
case "RS256", "EdDSA", "ES256": | |
return v.pubkey, nil | |
} | |
return nil, fmt.Errorf("unsupported signature algorithm '%s' for %T", unvalidatedToken.Method.Alg(), v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I don't like that part that much, it's just a bit forced due to implementing the jwt.SigningMethod
interface and therefore I can't change the return type by adding an ok value.
What I can do is adding a constructor for the agentSigningMethod verifying the key type and returning an error if it's not supported.
Here's the current plugins interface. For people reading it, let me know your thoughts on the interface ! Plugins interfaceDevelopers can develop server-side and client-side authentication plugins. Server-side plugins provide a Client-side plugins provide a /////////////////////////////////////
// Server auth plugins //
/////////////////////////////////////
// In ssh3, authorized_keys are replaced by authorized_identities where a use can specify classical
// public keys as well as other authentication and authorization methods such as OAUTH2 and SAML 2.0
type RequestIdentityVerifier interface {
Verify(request *http.Request, base64ConversationID string) bool
}
// parses an AuthorizedIdentity line (`identityStr`). Returns a new Identity and a nil error if the
// line was successfully parsed. Returns a nil identity and a nil error if the line format is unknown
// to the plugin. Returns a non-nil error if any other error that is worth to be logged occurs.
//
// plugins are currently a single function so that they are completely stateless
type ServerAuthPlugin func(username string, identityStr string) (RequestIdentityVerifier, error)
/////////////////////////////////////
// Client auth plugins //
/////////////////////////////////////
// returns all the suitable authentication methods to be tried against the server in the form
// of a slice of ClientAuthMethod. Every ClientAuthMethod will have the opportunity to prepare
// an HTTP request with authentication material to startup an SSH3 conversation. For instance,
// for pubkey authentication using the private key files on the filesystem, the
// GetClientAuthMethodsFunc can return a slice containing one ClientAuthMethod for
// each private key file it wants to try.
// if no SSH agent socket if found, sshAgent is nil
type GetClientAuthMethodsFunc func(request *http.Request, sshAgent agent.ExtendedAgent, clientConfig *client_config.Config, roundTripper *http3.RoundTripper) ([]ClientAuthMethod, error)
type ClientAuthMethod interface {
// PrepareRequestForAuth updated the provided request with the needed headers
// for authentication.
// The method must not alter the request method (must always be CONNECT) nor the
// Host/:origin, User-Agent or :path headers.
// The agent is the connected SSH agent if it exists, nil otherwise
// The provided roundTripper can be used to perform requests with the server to prepare
// the authentication process.
// username is the username to authenticate
// conversation is the Conversation we want to establish
PrepareRequestForAuth(request *http.Request, sshAgent agent.ExtendedAgent, roundTripper *http3.RoundTripper, username string, conversation *ssh3.Conversation) error
}
type ClientAuthPlugin struct {
// A plugin can define one or more new SSH3 config options.
// A new option is defined by providing a dedicated option parser.
// The key in PluginOptions must be a unique name for each option
// and must not conflict with any existing option
// (good practice: "<your_repo_name>[-<option_name>]")
PluginOptions map[client_config.OptionName]client_config.OptionParser
PluginFunc GetClientAuthMethodsFunc
} |
…th from the base client
Co-authored-by: Ethan Heilman <[email protected]>
Co-authored-by: Ethan Heilman <[email protected]>
@francoismichel Whats the right way for a plugin to only be used if a certain CLI option is provided? For instance if I wanted to do |
You'll have to devine a But in a nutshell, you'll have to implement a // OptionParser allows parsing an client config option from a string.
type OptionParser interface {
// returns the option config keyword
// This keyword is used when parsing the SSH config.
OptionConfigName() string
// returns the Option represented by this list of config values
// the values are all retrieved from the config.
// values contain several entries if the keyword (see `OptionConfigName`) is
// present several times in the config.
Parse(values []string) (Option, error)
}
// CLIOptionParser defines a parser that can be hooked in the CLI flags
type CLIOptionParser interface {
OptionParser
FlagName() string
Usage() string
// returns whether it should be considered as a boolean flag (parsed using flag.Bool(), with no flag value)
// or as a flag with a value. If IsBoolFlag returns true, the Parse() method will be called with "yes" or "no"
// depending of the boolean value of the arg
IsBoolFlag() bool
} And then, you can register your parser in your plugin's func init() {
plugin := auth.ClientAuthPlugin{
PluginOptions: map[config.OptionName]config.OptionParser{PRIVKEY_OPTION_NAME: &PrivkeyOptionParser{}},
PluginFunc: privkeyPluginFunc,
}
plugins.RegisterClientAuthPlugin("privkey_auth", plugin)
} Also, all ideas/suggestions to improve the current design are welcome. |
@francoismichel Thanks, that's helpful. I have been playing around with
What about having a more explicit way to enable/disable a plugin enforced as part of the plugin interface. I could see this being useful in situations where a user wants to skip a particular plugin entirely. Perhaps, the plugin isn't fedramp compliance or maybe that plugins func Dial(ctx context.Context, config *client_config.Config, qconn quic.EarlyConnection,
roundTripper *http3.RoundTripper,
sshAgent agent.ExtendedAgent)
...
// Use plugin interface to force plugin implementers to specify their plugins:
// - Enable flag --use-oidc
// - Disable flag --no-use-oidc
// and if the plugin is enabled by default
plugins := internal.GetClientAuthPlugins()
enabledPlugins := []auth.ClientAuthPlugin{}
for _, plugin := range plugins {
if IsPluginDisabledByFlag(config, plugin.DisableFlag()) {
continue
} else if IsPluginEnabledByFlag(config, plugin.EnableFlag()) || Plugin.EnabledByDefault() {
enabledPlugins = append(enabledPlugins, plugin)
continue
}
}
// Only call the enabled plugins
for _, plugin := range enabledPlugins {
authMethods, err := plugin.PluginFunc(req, sshAgent, config, roundTripper)
if err != nil {
return nil, err
}
for _, authMethod := range authMethods {
err = authMethod.PrepareRequestForAuth(req, sshAgent, roundTripper, config.Username(), conv)
... |
You mean, forcing plugins implementers to define CLI flags to entirely disable their plugins ? Your opinion ? |
I like that idea. Maybe this as well so that someone can cherry pick the plugins they want to use |
Merged ! We can look at the |
This PR is a first attempt for providing a pluggable authentication system.
The end goal is to allow compile-time plugins to implement new authentication behaviours (e.g. SAML, passkeys, ...).