Skip to content
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

Adds OpenPubkey Support to SSH3 #146

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cec99f2
add server-side plugins registry
francoismichel Mar 25, 2024
bfe0363
go fmt
francoismichel Mar 25, 2024
ec7fa7a
add full server-side auth plugins support
francoismichel Mar 26, 2024
1379bcc
add missing interface.go
francoismichel Mar 26, 2024
342e900
introduce HTTP request verifiers for server-side auth plugins
francoismichel Mar 26, 2024
7d3dd73
fix wrong type case in HandleAuth's switch
francoismichel Mar 26, 2024
83d4665
make plugins registry generic & add client plugins registry
francoismichel Mar 27, 2024
9bf304a
move auth/openid_connect.go in its own package
francoismichel Mar 27, 2024
3489106
add client pluggable config options
francoismichel Apr 16, 2024
30df154
implement the logic for using pluggable options
francoismichel Apr 17, 2024
ae0a7f8
struct Options -> struct Config
francoismichel Apr 17, 2024
9addcac
add missing client/config
francoismichel Apr 17, 2024
d4b153f
fix log formatting in ssh3 client
francoismichel Apr 17, 2024
cde645f
add the missing pieces to execute client-based plugins
francoismichel Apr 22, 2024
d83ff27
use plugins to perform priv/pubkey based auth and remove key-based au…
francoismichel Apr 22, 2024
256e05c
use the pubkey_authentication plugin by default in the ssh3 client
francoismichel Apr 22, 2024
5acf35b
avoid nil map from being returned by GetConfigForHost
francoismichel Apr 23, 2024
21342c3
implement server-side pubkey auth as a plugin and remove it from the …
francoismichel Apr 23, 2024
86b98d5
separate client and server key-based auth packages
francoismichel Apr 23, 2024
6f4d3a9
remove pubkey auth from server's base code and retrieve it from plugins
francoismichel Apr 23, 2024
c4a7167
GetAuthMethodsFunc -> GetClientAuthMethodsFunc
francoismichel Apr 23, 2024
c346ce1
reformat comments in plugins' interface.go
francoismichel Apr 23, 2024
92aeb1b
add doc comments in plugins' plugins.go
francoismichel Apr 23, 2024
f6d4b81
feat: Adds OpenPubkey to SSH3
EthanHeilman May 27, 2024
53ba24d
Changes to read from authorized identities
EthanHeilman May 29, 2024
cee963f
Adds error log statements to debug invalid JWT
EthanHeilman Jul 4, 2024
f5ae6a9
Fixes OSM kid issue by using lastest version of OPK
EthanHeilman Jul 4, 2024
6b71ba9
Adds check for the issuer and audience/clientId
EthanHeilman Aug 7, 2024
dc702db
Adds leeway for slightly broken clocks
EthanHeilman Aug 7, 2024
f46c487
Increases timer leeway to 2 minutes
EthanHeilman Aug 20, 2024
3ee4970
Enables CLI to enable openpubkey login
EthanHeilman Aug 21, 2024
286647d
Updates to latest openpubkey release v0.4.0
EthanHeilman Aug 21, 2024
ad02d32
Cleans up debug statements
EthanHeilman Aug 21, 2024
c72beec
Merge branch 'main' into opkssh3
EthanHeilman Sep 9, 2024
a76534c
Cleans after merge
EthanHeilman Sep 9, 2024
e2e25be
Removes unused build JWT functions
EthanHeilman Sep 9, 2024
4fa08bf
Fixes typo, changes auth identity tag
EthanHeilman Sep 10, 2024
98cf8bd
Cleaning up log messages
EthanHeilman Sep 10, 2024
3c0d877
Improves godocs
EthanHeilman Sep 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions auth/plugins/openpubkey/client/opk_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package openpubkey_authentication

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/francoismichel/ssh3"
"github.com/francoismichel/ssh3/auth"
"github.com/francoismichel/ssh3/auth/plugins"
"github.com/francoismichel/ssh3/client/config"
"github.com/golang-jwt/jwt/v5"
"github.com/openpubkey/openpubkey/client"
"github.com/openpubkey/openpubkey/providers"
"github.com/quic-go/quic-go/http3"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/ssh/agent"
)

func init() {
plugin := auth.ClientAuthPlugin{
PluginOptions: map[config.OptionName]config.OptionParser{OPENPUBKEY_OPTION_NAME: &OpenPubkeyOptionParser{}},
PluginFunc: openpubkeyPluginFunc,
}
plugins.RegisterClientAuthPlugin("openpubkey_auth", plugin)
}

const OPENPUBKEY_OPTION_NAME = "github.com/openpubkey/ssh3-openpubkey_auth"

// Implements client-side OpenPubkey authentication
type OpenPubkeyAuthOption struct {
issuer string
}

// Issuer returns the OpenID Provider issuer URI specified by the user
func (o *OpenPubkeyAuthOption) Issuer() string {
return o.issuer
}

// OpenPubkeyOptionParser handles SSH3 command line arguments relevant to OpenPubkey.
// An example command: `./ssh3 -openpubkey https://accounts.google.com [email protected]:443/ssh3-term`
type OpenPubkeyOptionParser struct{}

// FlagName implements config.CLIOptionParser.
func (*OpenPubkeyOptionParser) FlagName() string {
return "openpubkey"
}

// IsBoolFlag implements config.CLIOptionParser.
func (*OpenPubkeyOptionParser) IsBoolFlag() bool {
return false
}

// OptionConfigName implements config.OptionParser.
func (*OpenPubkeyOptionParser) OptionConfigName() string {
return "OpenPubkey"
}

// Parse implements config.OptionParser.
func (*OpenPubkeyOptionParser) Parse(values []string) (config.Option, error) {
return &OpenPubkeyAuthOption{
issuer: values[0],
}, nil
}

// Usage implements config.CLIOptionParser.
func (*OpenPubkeyOptionParser) Usage() string {
return "OpenID Provider to use"
}

var _ config.CLIOptionParser = &OpenPubkeyOptionParser{}

// openpubkeyPluginFunc is set as the PluginFunc for the plugin in init(). Its purpose
// is to:
// 1. read the config/options set by the SSH3 client,
// 2. determine if OpenPubkey auth would be appropriate given the config/options specified,
// 3. and if so, return the OpenPubkeyAuthMethod for the specified options/config.
var openpubkeyPluginFunc auth.GetClientAuthMethodsFunc = func(request *http.Request,
sshAgent agent.ExtendedAgent, clientConfig *config.Config,
roundTripper *http3.RoundTripper) ([]auth.ClientAuthMethod, error) {
for _, opt := range clientConfig.Options() {
if o, ok := opt.(*OpenPubkeyAuthOption); ok {
switch o.Issuer() {
// We only support Google
case "https://accounts.google.com":
providerOpts := providers.GetDefaultGoogleOpOptions()
providerOpts.GQSign = false
provider := providers.NewGoogleOpWithOptions(providerOpts)
methods := []auth.ClientAuthMethod{
&OpenPubkeyAuthMethod{
provider: provider,
}}
return methods, nil
// Add new OpenID Provider support here
default:
log.Error().Msgf("openID Provider is not supported by OpenPubkey: issuer=%s", o.Issuer())
return nil, nil
}
}
}
return nil, nil
}

// OpenPubkeyAuthMethod implements auth.ClientAuthMethod.
type OpenPubkeyAuthMethod struct {
provider providers.OpenIdProvider
}

// PrepareRequestForAuth implements auth.ClientAuthMethod.
// This function performs the client side of the OpenPubkey authentication.
func (o *OpenPubkeyAuthMethod) PrepareRequestForAuth(request *http.Request,
sshAgent agent.ExtendedAgent, roundTripper *http3.RoundTripper,
username string, conversation *ssh3.Conversation) error {
opkClient, err := client.New(o.provider)
if err != nil {
return err
}
pkt, err := opkClient.Auth(context.Background())
if err != nil {
return err
}
pktCom, err := pkt.Compact()
if err != nil {
return err
}
convID := conversation.ConversationID()
b64ConvID := base64.StdEncoding.EncodeToString(convID[:])
claims := jwt.MapClaims{
"iss": username,
"iat": jwt.NewNumericDate(time.Now()),
"exp": jwt.NewNumericDate(time.Now().Add(10 * time.Second)),
"sub": "ssh3",
"aud": "unused",
"client_id": fmt.Sprintf("ssh3-%s", username),
"jti": b64ConvID,
}
msg, err := json.Marshal(claims)
if err != nil {
return err
}
osm, err := pkt.NewSignedMessage(msg, opkClient.GetSigner())
if err != nil {
return err
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s#%s", osm, pktCom))
return nil
}
167 changes: 167 additions & 0 deletions auth/plugins/openpubkey/server/server_plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package server_openpubkey_authentication

import (
"context"
"fmt"
"net/http"
"strings"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/openpubkey/openpubkey/pktoken"
"github.com/openpubkey/openpubkey/providers"
"github.com/openpubkey/openpubkey/verifier"

"github.com/francoismichel/ssh3/auth"
"github.com/francoismichel/ssh3/auth/plugins"
"github.com/francoismichel/ssh3/server_auth"
"github.com/rs/zerolog/log"
)

const PLUGIN_NAME = "github.com/openpubkey/ssh3-server_openpubkey_auth"

// OPENPUBKEY_TAG specifies an identity string as an OpenPubkey identity string in the authorized_identities file.
const OPENPUBKEY_TAG = "openpubkey"

func init() {
if err := plugins.RegisterServerAuthPlugin(PLUGIN_NAME, OpenPubkeyAuthPlugin); err != nil {
log.Error().Msgf("could not register plugin %s: %s", PLUGIN_NAME, err)
}
}

// OpenPubkeyIdentityVerifier implements server-side OpenPubkey authentication.
type OpenPubkeyIdentityVerifier struct {
username string
clientIdOidc string
issuerOidc string
email string
}

// Verify authenticates a new SSH3 TLS connection using OpenPubkey.
// It does this by checking that:
// 1. a PK Token has been provided,
// 2. the identity in the PK Token matches an OpenPubkey identity in the authorized identities file,
// 3. the conversationID of the TLS connection has been signed by PK Token.
// If all these checks pass it accepts the SSH3 connection as the identity specified.
func (v *OpenPubkeyIdentityVerifier) Verify(request *http.Request, base64ConversationID string) bool {
authStr, wellFormattedB64Token := server_auth.ParseBearerAuth(request.Header.Get("Authorization"))
if !wellFormattedB64Token {
log.Error().Msgf("!wellFormattedB64Token %s ", request.Header.Get("Authorization"))
return false
}

authStrArr := strings.Split(authStr, "#")
if len(authStrArr) != 2 {
log.Error().Msgf("authStr not properly formed")
return false
}
jwtToken := authStrArr[0]
pktCom := authStrArr[1]

var provider providers.OpenIdProvider
// Add new OpenID Provider support here
switch v.issuerOidc {
case "https://accounts.google.com":
providerOpts := providers.GetDefaultGoogleOpOptions()
providerOpts.ClientID = v.clientIdOidc
providerOpts.GQSign = false
provider = providers.NewGoogleOpWithOptions(providerOpts)
default:
log.Error().Msgf("openID Provider is not supported by OpenPubkey: issuer=%s", v.issuerOidc)
return false
}
opkVerifier, err := verifier.New(provider)
if err != nil {
log.Error().Msgf("failed to configure openpubkey verifier: %s", err)
return false
}
pkt, err := pktoken.NewFromCompact([]byte(pktCom))
if err != nil {
log.Error().Msgf("failed to deserialize compact PK Token: %s", err)
return false
}
err = opkVerifier.VerifyPKToken(context.Background(), pkt)
if err != nil {
log.Error().Msgf("failed to verify PK Token: %s", err)
return false
}

if _, err := pkt.VerifySignedMessage([]byte(jwtToken)); err != nil {
log.Error().Msgf("openPubkey JWT signature verification failed: %s", err)
return false
}

cic, err := pkt.GetCicValues()
if err != nil {
log.Error().Msgf("openPubkey CIC is wrong: %s", err)
return false
}

upk := cic.PublicKey()
var rawkey interface{} // This is the raw key, such as *rsa.PrivateKey or *ecdsa.PrivateKey
if err := upk.Raw(&rawkey); err != nil {
log.Error().Msgf("openPubkey CIC is wrong: %s", err)
return false
}

token, err := jwt.Parse(jwtToken,
func(unvalidatedToken *jwt.Token) (interface{}, error) {
switch unvalidatedToken.Method.Alg() {
case "RS256", "EdDSA", "ES256":
return rawkey, nil
}
return nil, fmt.Errorf("unsupported signature algorithm '%s' for %T", unvalidatedToken.Method.Alg(), v)
},
jwt.WithIssuer(v.username),
jwt.WithSubject("ssh3"),
jwt.WithIssuedAt(),
jwt.WithLeeway(120*time.Second), // Be forgiving of small clock differences
jwt.WithAudience("unused"),
jwt.WithValidMethods([]string{"RS256", "EdDSA", "ES256"}))
if err != nil || !token.Valid {
log.Error().Msgf("invalid OpenPubkey signed JWT: %s", err)
return false
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
if _, ok = claims["exp"]; !ok {
log.Error().Msgf("missing exp")
return false
}
if clientId, ok := claims["client_id"]; !ok || clientId != fmt.Sprintf("ssh3-%s", v.username) {
log.Error().Msgf("invalid client_id %s", clientId)
return false
}
if jti, ok := claims["jti"]; !ok || jti != base64ConversationID {
log.Error().Msgf("rsa verification failed: the jti claim does not contain the base64-encoded conversation ID")
return false
}
} else {
log.Error().Msgf("bad JWT claims type: %T", token.Claims)
return false
}
return true
}

// OpenPubkeyAuthPlugin takes a username and identityStr from the authorized_identities file
// and either rejects the identity string or returns a verifier. This function is used to
// search through the authorized_identities file to find a matching authorized identities.
// An identity string matches if matches on the username and it is tagged as openpubkey.
func OpenPubkeyAuthPlugin(username string, identityStr string) (auth.RequestIdentityVerifier, error) {
log.Debug().Msgf("OpenPubkey auth plugin: parse identity string %s", identityStr)

identityStrArr := strings.Split(identityStr, " ")
if len(identityStrArr) != 4 || identityStrArr[0] != OPENPUBKEY_TAG {
log.Debug().Msgf("the identity string is not a compatible openpubkey string, %s", identityStr)
return nil, nil
}
clientId := identityStrArr[1]
issuer := identityStrArr[2]
email := identityStrArr[3]

return &OpenPubkeyIdentityVerifier{
username: username,
clientIdOidc: clientId,
issuerOidc: issuer,
email: email,
}, nil
}
1 change: 1 addition & 0 deletions cmd/plugin_endpoint/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
_ "github.com/francoismichel/ssh3/auth/plugins/openpubkey/client"
_ "github.com/francoismichel/ssh3/auth/plugins/pubkey_authentication/client"
cmd "github.com/francoismichel/ssh3/cmd"
)
Expand Down
1 change: 1 addition & 0 deletions cmd/ssh3-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

// authentication plugins
_ "github.com/francoismichel/ssh3/auth/plugins/openpubkey/server"
_ "github.com/francoismichel/ssh3/auth/plugins/pubkey_authentication/server"
)

Expand Down
1 change: 1 addition & 0 deletions cmd/ssh3/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

// authentication plugins
_ "github.com/francoismichel/ssh3/auth/plugins/openpubkey/client"
_ "github.com/francoismichel/ssh3/auth/plugins/pubkey_authentication/client"
)

Expand Down
Loading