Golang的旋转键
#安全 #go #cryptography

这里的想法是为我们的令牌签名旋转ED25519键。通过这种方式,我们通过不对称签名实现了更大的安全性和不可预测性。

首先,让我们组装我们的容器。

// Dockerfile
FROM golang:1.20-alpine

WORKDIR /app
COPY go.* .
RUN go mod tidy
COPY . .
RUN go build -buildvcs=false -ldflags '-s -w' -o ./bin ./cmd/guardian

EXPOSE 4000

CMD [ "./bin/guardian" ]
// docker-compose.yml
version: '3'

networks:
  guardian:
    driver: bridge

services:
  cache:
    image: redis:7.0-alpine
    hostname: cache.guardian.local
    restart: always
    ports:
      - '6379:6379'
    networks:
      - guardian
  app:
    platform: linux/amd64
    build: .
    hostname: app.guardian.local
    depends_on:
      - cache
    networks:
      - guardian
    ports:
      - 4000:4000

密钥服务器模块将负责设置最后一个密钥ID,使用最后生成的密钥生成令牌,并在给定的间隔中旋转键。
kid是所讨论的密钥的标识符。每次生成令牌时,我们都会使用REDIS中存储的最后一个密钥。主文件中的我们的ks.Start()工人将旋转每个5 minutes

// internal/key-server/key-server.go
package keyserver

import (
    "context"
    "crypto/ed25519"
    "crypto/rand"
    "crypto/x509"
    "errors"
    "time"

    "github.com/golang-jwt/jwt"
    gonanoid "github.com/matoous/go-nanoid/v2"
    "github.com/redis/go-redis/v9"
)

type KeyServer struct {
    cache *redis.Client
}

func New(cache *redis.Client) *KeyServer {
    return &KeyServer{
        cache: cache,
    }
}

func (ks *KeyServer) NewSignedToken(ctx context.Context, data map[string]interface{}) (string, jwt.Claims, error) {
    lastKID, err := ks.GetLastKID(ctx)
    if err != nil {
        return "", nil, err
    }

    lastKey, err := ks.GetKeyByKID(ctx, lastKID)
    if err != nil {
        return "", nil, err
    }

    claims := jwt.MapClaims{
        "exp": time.Now().Add(15 * time.Minute).Unix(),
        "iss": "guardian",
        "kid": lastKID,
        "dat": data,
    }
    token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
    signedToken, err := token.SignedString(lastKey)

    return signedToken, claims, err
}

func (ks *KeyServer) DecodeSignedToken(ctx context.Context, signedToken string) (*jwt.Token, jwt.Claims, error) {
    claims := jwt.MapClaims{}
    token, err := jwt.ParseWithClaims(signedToken, &claims, func(t *jwt.Token) (interface{}, error) {
        claims, ok := t.Claims.(*jwt.MapClaims)
        if !ok {
            return nil, errors.New("invalid token")
        }

        key, err := ks.GetKeyByKID(ctx, (*claims)["kid"].(string))
        if err != nil {
            return nil, err
        }

        return key.Public(), nil
    })

    if err != nil {
        return nil, nil, err
    }

    return token, claims, nil
}

func (ks *KeyServer) SetLastKID(ctx context.Context, kid string) error {
    return ks.cache.Set(ctx, "last-kid", kid, time.Minute*15).Err()
}

func (ks *KeyServer) SetLastKey(ctx context.Context, kid string, pvk ed25519.PrivateKey) error {
    b, err := x509.MarshalPKCS8PrivateKey(pvk)
    if err != nil {
        return err
    }

    if err := ks.cache.Set(ctx, kid, b, time.Minute*15).Err(); err != nil {
        return err
    }

    return nil
}

func (ks *KeyServer) GetKeyByKID(ctx context.Context, kid string) (ed25519.PrivateKey, error) {
    key, err := ks.cache.Get(ctx, kid).Result()
    if err != nil {
        return nil, err
    }

    pvk, err := x509.ParsePKCS8PrivateKey([]byte(key))
    if err != nil {
        return nil, err
    }

    return pvk.(ed25519.PrivateKey), nil
}

func (ks *KeyServer) GetLastKID(ctx context.Context) (string, error) {
    lastKID, err := ks.cache.Get(ctx, "last-kid").Result()
    if err != nil {
        return "", err
    }

    return lastKID, nil
}

func (ks *KeyServer) Rotate(ctx context.Context) error {
    kid, err := gonanoid.New(8)
    if err != nil {
        return err
    }

    _, pvk, err := ed25519.GenerateKey(rand.Reader)
    if err != nil {
        return err
    }

    if err := ks.SetLastKID(ctx, kid); err != nil {
        return err
    }

    if err := ks.SetLastKey(ctx, kid, pvk); err != nil {
        return err
    }

    return nil
}

func (ks *KeyServer) Start(ctx context.Context) error {
    ks.Rotate(ctx)

    for range time.Tick(time.Minute * 5) {
        ks.Rotate(ctx)
    }

    return nil
}

POST /api/v1/tokens路线将通过发送的claims defined in the body(json)生成一个令牌。 GET /api/v1/tokens路线只能充当我们测试令牌的验证者。
令牌用private ed2559 key签名并用public key of that same key验证。

// cmd/guardian/main.go
package main

import (
    "context"
    "log"
    "net/http"

    keyserver "github.com/ElioenaiFerrari/guardian/internal/key-server"
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/gofiber/fiber/v2/middleware/recover"
    "github.com/redis/go-redis/v9"
)

func main() {
    cache := redis.NewClient(&redis.Options{
        Addr:     "cache.guardian.local:6379",
        DB:       0,
        Password: "",
    })
    ks := keyserver.New(cache)
    ctx := context.Background()

    go ks.Start(ctx)

    app := fiber.New()
    app.Use(logger.New())
    app.Use(recover.New())
    app.Use(cors.New())

    v1 := app.Group("/api/v1")
    v1.Post("/tokens", func(c *fiber.Ctx) error {
        var data map[string]interface{}

        if err := c.BodyParser(&data); err != nil {
            return fiber.NewError(http.StatusBadRequest, err.Error())
        }

        token, claims, err := ks.NewSignedToken(ctx, data)
        if err != nil {
            return fiber.NewError(http.StatusInternalServerError, err.Error())
        }

        return c.Status(http.StatusCreated).JSON(fiber.Map{
            "token":  token,
            "claims": claims,
        })
    })

    v1.Get("/tokens", func(c *fiber.Ctx) error {
        signedToken := c.Get("Authorization")
        if signedToken == "" {
            return fiber.NewError(http.StatusUnauthorized, "unauthorized")
        }

        token, claims, err := ks.DecodeSignedToken(ctx, signedToken)
        if err != nil {
            return fiber.NewError(http.StatusUnauthorized, err.Error())
        }

        return c.Status(http.StatusCreated).JSON(fiber.Map{
            "token":  token,
            "claims": claims,
        })
    })

    log.Fatal(app.Listen(":4000"))
}

让我们启动我们的服务器。

docker-compose up --build

例子:

  • 发布请求
curl --location 'http://localhost:4000/api/v1/tokens' \
--header 'Content-Type: application/json' \
--data '{
    "sub": "123123123"
}'
  • 帖子回复
{
    "claims": {
        "dat": {
            "sub": "123123123"
        },
        "exp": 1678807427,
        "iss": "guardian",
        "kid": "rwqgN9dI"
    },
    "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJkYXQiOnsic3ViIjoiMTIzMTIzMTIzIn0sImV4cCI6MTY3ODgwNzQyNywiaXNzIjoiZ3VhcmRpYW4iLCJraWQiOiJyd3FnTjlkSSJ9.8yUZ-0XSZk5UMITSUZLX2SXIs5MIIXCID4X4IBxWy_N9WHXCBk0v6o0urB5r7Sr8vz7T_Z_2JVsNU4j681PMAw"
}
  • 获取请求
curl --location 'http://localhost:4000/api/v1/tokens' \
--header 'Authorization: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJkYXQiOnsic3ViIjoiMTIzMTIzMTIzIn0sImV4cCI6MTY3ODgwNzQyNywiaXNzIjoiZ3VhcmRpYW4iLCJraWQiOiJyd3FnTjlkSSJ9.8yUZ-0XSZk5UMITSUZLX2SXIs5MIIXCID4X4IBxWy_N9WHXCBk0v6o0urB5r7Sr8vz7T_Z_2JVsNU4j681PMAw'
  • 得到回应
{
    "claims": {
        "dat": {
            "sub": "123123123"
        },
        "exp": 1678807427,
        "iss": "guardian",
        "kid": "rwqgN9dI"
    },
    "token": {
        "Raw": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJkYXQiOnsic3ViIjoiMTIzMTIzMTIzIn0sImV4cCI6MTY3ODgwNzQyNywiaXNzIjoiZ3VhcmRpYW4iLCJraWQiOiJyd3FnTjlkSSJ9.8yUZ-0XSZk5UMITSUZLX2SXIs5MIIXCID4X4IBxWy_N9WHXCBk0v6o0urB5r7Sr8vz7T_Z_2JVsNU4j681PMAw",
        "Method": {},
        "Header": {
            "alg": "EdDSA",
            "typ": "JWT"
        },
        "Claims": {
            "dat": {
                "sub": "123123123"
            },
            "exp": 1678807427,
            "iss": "guardian",
            "kid": "rwqgN9dI"
        },
        "Signature": "8yUZ-0XSZk5UMITSUZLX2SXIs5MIIXCID4X4IBxWy_N9WHXCBk0v6o0urB5r7Sr8vz7T_Z_2JVsNU4j681PMAw",
        "Valid": true
    }
}

repository给我一个明星,并为我们的group做出了贡献。还要看我的portfolio