added RSA and EDDSA

This commit is contained in:
Ethan Knowlton 2023-11-04 22:56:51 -04:00
parent bc03347fec
commit 45192d651f
9 changed files with 177 additions and 22 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
.idea/
/bin/
.envrc
/keys

View File

@ -109,6 +109,8 @@ $ echoip
Configuration is managed in the `etc/echoip/config.toml` file. This file should be located in the `/etc` folder on your server ( /etc/echoip/config.toml ). If you have the project on your server, you can run `make install-config` to copy it the right location.
***Change location of config file with the `echoip -c /path/to/config.toml` flag***
```toml
Listen = ":8080"
TemplateDir = "html" # The directory of the template files ( eg, index.html )
@ -166,11 +168,12 @@ ECHOIP_JWT_SECRET="HS256"
You can authenticate each API request with JWT token.
Just enable `config.Jwt.Enabled` and add your JWT secret to `config.Jwt.Secret`.
EchoIP validates JWT signing algorithm, `config.SigningMethod` should be one of available from `golang-jwt/jwt` and match your expceted algorithm. Currently only supporting HMAC signing.
EchoIP validates JWT signing algorithm, `config.SigningMethod` should be one of available from `golang-jwt/jwt` and match your expceted algorithm.
`config.SigningMethod string`
```
# ES256 | ES384 | ES512
# RS256 | RS384 | RS512
# HS256 | HS384 | HS512
```
@ -178,6 +181,8 @@ Requests will be accepted if a valid token is provided in `Authorization: Bearer
A `401` will be returned should the token not be valid.
***You can convert a key created with ssh-keygen using something like `ssh-keygen -f id_rsa.pub -e -mpem`***
### Caching with Redis
You can connect EchoIP to a Redis client to cache each request per IP. You can configure the life of the key in `config.CacheTtl`.

View File

@ -1,6 +1,7 @@
package main
import (
"flag"
"io"
"log"
"strings"
@ -35,20 +36,25 @@ func init() {
}
func main() {
var configPath string
flag.StringVar(&configPath, "c", "/etc/echoip/config.toml", "Path to config file ( /etc/echoip/config.toml )")
flag.Parse()
runConfig, err := config.GetConfig()
if err != nil {
log.Fatalf("Error building configuration: %s", err)
}
file, err := os.Open("/etc/echoip/config.toml")
defer file.Close()
log.Printf("Using config file %s", configPath)
configFile, err := os.Open(configPath)
defer configFile.Close()
if err != nil {
log.Printf("Error opening config file (/etc/echoip/config.toml): %s", err)
} else {
var b []byte
b, err = io.ReadAll(file)
b, err = io.ReadAll(configFile)
if err != nil {
log.Printf("Error reading config file (/etc/echoip/config.toml): %s", err)
}
@ -102,6 +108,18 @@ func main() {
serverCache = &cache.Null{}
}
if len(runConfig.Jwt.PublicKey) != 0 {
log.Printf("Loading public key from %s", runConfig.Jwt.PublicKey)
pubKey, err := os.ReadFile(runConfig.Jwt.PublicKey)
if err != nil {
log.Fatal(err)
}
runConfig.Jwt.PublicKeyData = pubKey
}
server := http.New(parser, serverCache, &runConfig)
server.IPHeaders = runConfig.TrustedHeaders

View File

@ -22,6 +22,8 @@ type Jwt struct {
Enabled bool
SigningMethod string
Secret string
PublicKey string
PublicKeyData []byte
}
type Config struct {

5
go.mod
View File

@ -9,17 +9,22 @@ require (
github.com/oschwald/geoip2-golang v1.5.0
github.com/qioalice/ipstack v1.0.1
github.com/redis/go-redis/v9 v9.2.1
github.com/stretchr/testify v1.8.1
gopkg.in/stretchr/testify.v1 v1.2.2
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/oschwald/maxminddb-golang v1.8.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.11.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

6
go.sum
View File

@ -45,9 +45,11 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@ -86,6 +88,7 @@ github.com/qioalice/ipstack v1.0.1/go.mod h1:6eB9LdNCUdUoOsfDB8Pn2GpmD2I+f2k3yR3
github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@ -203,9 +206,12 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -7,13 +7,11 @@ import (
"html/template"
"log"
"path/filepath"
"reflect"
"strings"
"net/http/pprof"
rcache "github.com/go-redis/cache/v9"
"github.com/golang-jwt/jwt"
"github.com/levelsoftware/echoip/cache"
"github.com/levelsoftware/echoip/config"
parser "github.com/levelsoftware/echoip/iputil/paser"
@ -426,21 +424,8 @@ func handleAuth(r *http.Request, runConfig *config.Config) error {
authorization := r.Header.Get("Authorization")
tokenString := strings.ReplaceAll(authorization, "Bearer ", "")
if _, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
expected := reflect.TypeOf(jwt.GetSigningMethod(runConfig.Jwt.SigningMethod))
got := reflect.TypeOf(token.Method)
if expected != got {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// Only support SigningMethodHMAC ( Others will be quite a bit more complicated )
return []byte(runConfig.Jwt.Secret), nil
}); err != nil {
if runConfig.Debug {
log.Printf("Error validating token ( %s ): %s \n", tokenString, err)
}
return new(InvalidTokenError)
if err := ParseJWT(runConfig, tokenString); err != nil {
return err
}
return nil

69
http/jwt.go Normal file
View File

@ -0,0 +1,69 @@
package http
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"reflect"
"github.com/golang-jwt/jwt"
"github.com/levelsoftware/echoip/config"
)
func ParseJWT(runConfig *config.Config, tokenString string) error {
if _, err := jwt.Parse(tokenString, GetTokenKey(runConfig)); err != nil {
if runConfig.Debug {
log.Printf("Error validating token ( %s ): %s \n", tokenString, err)
}
return new(InvalidTokenError)
}
return nil
}
func GetTokenKey(runConfig *config.Config) func(token *jwt.Token) (interface{}, error) {
signingMethod := jwt.GetSigningMethod(runConfig.Jwt.SigningMethod)
var key interface{}
switch signingMethod.Alg() {
case "ES256", "ES384", "ES512":
pubKey, _ := GetECDSAKey(runConfig.Jwt.PublicKeyData)
key = pubKey
case "RS256", "RS384", "RS512":
pubKey, _ := GetRSAKey(runConfig.Jwt.PublicKeyData)
key = pubKey
default:
key = []byte(runConfig.Jwt.Secret)
}
return func(token *jwt.Token) (interface{}, error) {
expected := reflect.TypeOf(signingMethod)
got := reflect.TypeOf(token.Method)
if expected != got {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return key, nil
}
}
func GetECDSAKey(data []byte) (*ecdsa.PublicKey, error) {
pkiBlock, _ := pem.Decode(data)
var publicKey *ecdsa.PublicKey
pubInterface, _ := x509.ParsePKIXPublicKey(pkiBlock.Bytes)
publicKey = pubInterface.(*ecdsa.PublicKey)
return publicKey, nil
}
func GetRSAKey(data []byte) (*rsa.PublicKey, error) {
pkiBlock, _ := pem.Decode(data)
var publicKey *rsa.PublicKey
pubInterface, _ := x509.ParsePKIXPublicKey(pkiBlock.Bytes)
publicKey = pubInterface.(*rsa.PublicKey)
return publicKey, nil
}

64
http/jwt_test.go Normal file
View File

@ -0,0 +1,64 @@
package http
import (
"testing"
"github.com/levelsoftware/echoip/config"
"github.com/stretchr/testify/assert"
)
const ecdsaPublicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----`
const ecdsaToken = `eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA`
const rsaPublicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----`
const rsaToken = `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ`
const hmacKey = `supersecretkey`
const hmacToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.1Oi8c8PNZri8BTWg2oWXQVLCFNzI5b8uSeweKhpAIoc`
func TestGetTokenKeyWithECDSAKey(t *testing.T) {
err := ParseJWT(&config.Config{
Debug: true,
Jwt: config.Jwt{
SigningMethod: "ES256",
PublicKeyData: []byte(ecdsaPublicKey),
},
}, ecdsaToken)
assert.Nil(t, err)
}
func TestGetTokenKeyWithRSAKey(t *testing.T) {
err := ParseJWT(&config.Config{
Debug: true,
Jwt: config.Jwt{
SigningMethod: "RS256",
PublicKeyData: []byte(rsaPublicKey),
},
}, rsaToken)
assert.Nil(t, err)
}
func TestGetTokenKeyWithHMACKey(t *testing.T) {
err := ParseJWT(&config.Config{
Debug: true,
Jwt: config.Jwt{
SigningMethod: "HS256",
Secret: hmacKey,
},
}, hmacToken)
assert.Nil(t, err)
}