mirror of https://github.com/mpolden/echoip
added RSA and EDDSA
This commit is contained in:
parent
bc03347fec
commit
45192d651f
|
@ -5,3 +5,4 @@
|
|||
.idea/
|
||||
/bin/
|
||||
.envrc
|
||||
/keys
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ type Jwt struct {
|
|||
Enabled bool
|
||||
SigningMethod string
|
||||
Secret string
|
||||
PublicKey string
|
||||
PublicKeyData []byte
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
|
|
5
go.mod
5
go.mod
|
@ -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
6
go.sum
|
@ -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=
|
||||
|
|
19
http/http.go
19
http/http.go
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue