Skip to content

exp/services/webauth: add SEP-10 v1.2.0 implementation #2074

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

Merged
merged 31 commits into from
Dec 20, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
38b769c
exp/services/webauth: add SEP-10 implementation
leighmcculloch Dec 10, 2019
b738015
Revert back to what is required by current SEP-10
leighmcculloch Dec 18, 2019
41ea106
feedback from @ire-and-cursers
leighmcculloch Dec 18, 2019
4f60806
Change network default to test to align with horizon url
leighmcculloch Dec 18, 2019
d19b9c4
Add README
leighmcculloch Dec 18, 2019
a5ce7fa
Remove go1.13 specific dependency
leighmcculloch Dec 18, 2019
fa8c22c
Move jwtkey to an exp package
leighmcculloch Dec 18, 2019
223e13b
Move jwtkey to an exp/support package
leighmcculloch Dec 18, 2019
fa66f33
Add links and why to readme
leighmcculloch Dec 18, 2019
5e8bf2e
support/http/httpdecode: add form decoding helper
leighmcculloch Dec 19, 2019
dbe1522
support/http/httpdecode: add form+json combined decoding helper
leighmcculloch Dec 19, 2019
78e6d1d
Merge branch 'httpdecode-formurlencoded' into sep10webauth-base
leighmcculloch Dec 19, 2019
d172565
Use httpdecode
leighmcculloch Dec 19, 2019
d014da9
Remove unnecessary checks
leighmcculloch Dec 19, 2019
69b05e2
Fix duration in application options
leighmcculloch Dec 19, 2019
f5575e5
Fix unnecessary import
leighmcculloch Dec 19, 2019
1f0457d
Update README
leighmcculloch Dec 19, 2019
271b5aa
Rename Main to Run
leighmcculloch Dec 19, 2019
a45dfe6
Rename Run to Serve
leighmcculloch Dec 19, 2019
b5a7734
Merge branch 'master' into sep10webauth-base
leighmcculloch Dec 19, 2019
5beab66
Remove unused error
leighmcculloch Dec 20, 2019
03dbca2
Fix logger log ref
leighmcculloch Dec 20, 2019
97d33e5
Put jwtkeygen into a separate tool and link it into the server
leighmcculloch Dec 20, 2019
6aba5cc
Revert "Put jwtkeygen into a separate tool and link it into the server"
leighmcculloch Dec 20, 2019
a1a8106
Wrap error
leighmcculloch Dec 20, 2019
6b7e851
Add missing return
leighmcculloch Dec 20, 2019
8a11519
Keep logging in one function and pass around errors instead
leighmcculloch Dec 20, 2019
79c0fee
Add http.Status* instead of numbers
leighmcculloch Dec 20, 2019
6dc1db5
Add http.Status* instead of numbers
leighmcculloch Dec 20, 2019
abad794
Add logic to check master key is a signer with high threshold
leighmcculloch Dec 20, 2019
100b32f
Update README
leighmcculloch Dec 20, 2019
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
44 changes: 44 additions & 0 deletions exp/services/webauth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# webauth

This is a SEP-10 Web Authentication implementation based on SEP-10 v1.2.0.

This implementation is not polished and is still experimental. Running this implementation in production is not recommended.

## Usage

````
$ webauth --help
SEP-10 Web Authentication Server

Usage:
webauth [command] [flags]
webauth [command]

Available Commands:
genjwtkey Generate a JWT ECDSA key
serve Run the SEP-10 Web Authentication server

Flags:
-h, --help help for webauth

Use "webauth [command] --help" for more information about a command.
```

## Usage: Serve

```
$ webauth serve --help
Run the SEP-10 Web Authentication server

Usage:
webauth serve [flags]

Flags:
--challenge-expires-in int The time period after which the challenge transaction expires (default 300000000000)
--horizon-url string Horizon URL used for looking up account details (default "https://horizon-testnet.stellar.org/")
--jwt-expires-in int The time period that after which the JWT expires (default 18000000000000)
--jwt-key string Base64 encoded ECDSA private key used for signing JWTs
--network-passphrase string Network passphrase of the Stellar network transactions should be signed for (default "Test SDF Network ; September 2015")
--port int Port to listen and serve on (default 8000)
--signing-key string Stellar signing key used for signing transactions
```
41 changes: 41 additions & 0 deletions exp/services/webauth/internal/commands/genjwtkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package commands

import (
"github.com/spf13/cobra"
"github.com/stellar/go/exp/services/webauth/internal/jwtkey"
supportlog "github.com/stellar/go/support/log"
)

type GenJWTKeyCommand struct {
Logger *supportlog.Entry
}

func (c *GenJWTKeyCommand) Command() *cobra.Command {
cmd := &cobra.Command{
Use: "genjwtkey",
Short: "Generate a JWT ECDSA key",
Run: func(_ *cobra.Command, _ []string) {
c.Run()
},
}
return cmd
}

func (c *GenJWTKeyCommand) Run() {
k, err := jwtkey.GenerateKey()
if err != nil {
c.Logger.Fatal(err)
}

if public, err := jwtkey.PublicKeyToString(&k.PublicKey); err == nil {
c.Logger.Print("Public:", public)
} else {
c.Logger.Print("Public:", err)
}

if private, err := jwtkey.PrivateKeyToString(k); err == nil {
c.Logger.Print("Private:", private)
} else {
c.Logger.Print("Private:", err)
}
}
96 changes: 96 additions & 0 deletions exp/services/webauth/internal/commands/serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package commands

import (
"go/types"
"time"

"github.com/spf13/cobra"
"github.com/stellar/go/clients/horizonclient"
"github.com/stellar/go/exp/services/webauth/internal/serve"
"github.com/stellar/go/network"
"github.com/stellar/go/support/config"
supportlog "github.com/stellar/go/support/log"
)

type ServeCommand struct {
Logger *supportlog.Entry
}

func (c *ServeCommand) Command() *cobra.Command {
opts := serve.Options{
Logger: c.Logger,
}
configOpts := config.ConfigOptions{
{
Name: "port",
Usage: "Port to listen and serve on",
OptType: types.Int,
ConfigKey: &opts.Port,
FlagDefault: 8000,
Required: true,
},
{
Name: "horizon-url",
Usage: "Horizon URL used for looking up account details",
OptType: types.String,
ConfigKey: &opts.HorizonURL,
FlagDefault: horizonclient.DefaultTestNetClient.HorizonURL,
Required: true,
},
{
Name: "network-passphrase",
Usage: "Network passphrase of the Stellar network transactions should be signed for",
OptType: types.String,
ConfigKey: &opts.NetworkPassphrase,
FlagDefault: network.TestNetworkPassphrase,
Required: true,
},
{
Name: "signing-key",
Usage: "Stellar signing key used for signing transactions",
OptType: types.String,
ConfigKey: &opts.SigningKey,
Required: true,
},
{
Name: "challenge-expires-in",
Usage: "The time period after which the challenge transaction expires",
OptType: types.Int,
CustomSetValue: config.SetDuration,
ConfigKey: &opts.ChallengeExpiresIn,
FlagDefault: int(300 * time.Second),
Required: true,
},
{
Name: "jwt-key",
Usage: "Base64 encoded ECDSA private key used for signing JWTs",
OptType: types.String,
ConfigKey: &opts.JWTPrivateKey,
Required: true,
},
{
Name: "jwt-expires-in",
Usage: "The time period that after which the JWT expires",
OptType: types.Int,
CustomSetValue: config.SetDuration,
ConfigKey: &opts.JWTExpiresIn,
FlagDefault: int(300 * time.Minute),
Required: true,
},
}
cmd := &cobra.Command{
Use: "serve",
Short: "Run the SEP-10 Web Authentication server",
Run: func(_ *cobra.Command, _ []string) {
configOpts.Require()
configOpts.SetValues()
c.Run(opts)
},
}
configOpts.Init(cmd)
return cmd
}

func (c *ServeCommand) Run(opts serve.Options) {
serve.Main(opts)
}
80 changes: 80 additions & 0 deletions exp/services/webauth/internal/jwtkey/jwtkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Package jwtkey provides utility functions for generating, serializing and
// deserializing JWT ECDSA keys.
//
// TODO: Replace EC function usages with PKCS8 functions for supporting ECDSA
// and RSA keys instead of only supporting ECDSA. The fact this package only
// supports ECDSA is unnecessary.
package jwtkey

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/base64"

"github.com/stellar/go/support/errors"
)

// GenerateKey is a convenience function for generating an ECDSA key for use as
// a JWT key. It uses the P256 curve. To use other curves use the crypto/ecdsa
// package directly.
func GenerateKey() (*ecdsa.PrivateKey, error) {
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
return k, nil
}

// PrivateKeyToString converts a ECDSA private key into a ASN.1 DER and base64
// encoded string.
func PrivateKeyToString(k *ecdsa.PrivateKey) (string, error) {
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
return "", errors.Wrap(err, "marshaling ECDSA private key")
}
return base64.StdEncoding.EncodeToString(b), nil
}

// PublicKeyToString converts a ECDSA public key into a ASN.1 DER and base64
// encoded string.
func PublicKeyToString(k *ecdsa.PublicKey) (string, error) {
b, err := x509.MarshalPKIXPublicKey(k)
if err != nil {
return "", errors.Wrap(err, "marshaling ECDSA public key")
}
return base64.StdEncoding.EncodeToString(b), nil
}

// PrivateKeyFromString converts a ECDSA private key from a ASN.1 DER and
// base64 encoded string into a type.
func PrivateKeyFromString(s string) (*ecdsa.PrivateKey, error) {
keyBytes, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, errors.Wrap(err, "base64 decoding ECDSA private key")
}
key, err := x509.ParseECPrivateKey(keyBytes)
if err != nil {
return nil, errors.Wrap(err, "unmarshaling ECDSA private key")
}
return key, nil
}

// PublicKeyFromString converts a ECDSA public key from a ASN.1 DER and base64
// encoded string into a type.
func PublicKeyFromString(s string) (*ecdsa.PublicKey, error) {
keyBytes, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, errors.Wrap(err, "base64 decoding ECDSA public key")
}
keyI, err := x509.ParsePKIXPublicKey(keyBytes)
if err != nil {
return nil, errors.Wrap(err, "unmarshaling ECDSA public key")
}
key, ok := keyI.(*ecdsa.PublicKey)
if !ok {
return nil, errors.Wrap(err, "public key not ECDSA key")
}
return key, nil
}
66 changes: 66 additions & 0 deletions exp/services/webauth/internal/jwtkey/jwtkey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package jwtkey

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/base64"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGenerate(t *testing.T) {
key, err := GenerateKey()
require.NoError(t, err)
assert.Equal(t, elliptic.P256(), key.Curve)
}

func TestToFromStringRoundTrip(t *testing.T) {
testCases := []struct {
Name string
Curve elliptic.Curve
}{
{Name: "P224", Curve: elliptic.P224()},
{Name: "P256", Curve: elliptic.P256()},
{Name: "P384", Curve: elliptic.P384()},
{Name: "P521", Curve: elliptic.P521()},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
privateKey, err := ecdsa.GenerateKey(tc.Curve, rand.Reader)
require.NoError(t, err)

t.Run("private", func(t *testing.T) {
privateKeyStr, err := PrivateKeyToString(privateKey)
require.NoError(t, err)

// Private key as string should be valid standard base64
_, err = base64.StdEncoding.DecodeString(privateKeyStr)
require.NoError(t, err)

// Private key should decode back to the original
privateKeyRoundTripped, err := PrivateKeyFromString(privateKeyStr)
require.NoError(t, err)
assert.Equal(t, privateKey, privateKeyRoundTripped)
})

publicKey := &privateKey.PublicKey

t.Run("public", func(t *testing.T) {
publicKeyStr, err := PublicKeyToString(publicKey)
require.NoError(t, err)

// Public key as string should be valid standard base64
_, err = base64.StdEncoding.DecodeString(publicKeyStr)
require.NoError(t, err)

// Public key should decode back to the original
publicKeyRoundTripped, err := PublicKeyFromString(publicKeyStr)
require.NoError(t, err)
assert.Equal(t, publicKey, publicKeyRoundTripped)
})
})
}
}
56 changes: 56 additions & 0 deletions exp/services/webauth/internal/serve/challenge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package serve

import (
"net/http"
"time"

"github.com/stellar/go/keypair"
"github.com/stellar/go/strkey"
supportlog "github.com/stellar/go/support/log"
"github.com/stellar/go/support/render/httpjson"
"github.com/stellar/go/txnbuild"
)

// ChallengeHandler implements the SEP-10 challenge endpoint and handles
// requests for a new challenge transaction.
type challengeHandler struct {
Logger *supportlog.Entry
ServerName string
NetworkPassphrase string
SigningKey *keypair.Full
ChallengeExpiresIn time.Duration
}

type challengeResponse struct {
Transaction string `json:"transaction"`
NetworkPassphrase string `json:"network_passphrase"`
}

func (h challengeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

account := r.URL.Query().Get("account")
if !strkey.IsValidEd25519PublicKey(account) {
badRequest.Render(w)
return
}

tx, err := txnbuild.BuildChallengeTx(
h.SigningKey.Seed(),
account,
h.ServerName,
h.NetworkPassphrase,
h.ChallengeExpiresIn,
)
if err != nil {
h.Logger.Ctx(ctx).WithStack(err).Error(err)
serverError.Render(w)
return
}

res := challengeResponse{
Transaction: tx,
NetworkPassphrase: h.NetworkPassphrase,
}
httpjson.Render(w, res, httpjson.JSON)
}
Loading