mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-07-22 09:27:18 +00:00
modules/jwtx: Add verificationkey.go, savePublicKey() function
This commit momentarily adds dead code, which for now is only used in tests. The actual use case is going to be added in a follow up, but this commit is still kept separate for clarity.
This commit is contained in:
parent
cffdf6d6ca
commit
fa86259403
3 changed files with 378 additions and 8 deletions
|
|
@ -35,8 +35,8 @@ type SigningKeyCfg struct {
|
|||
}
|
||||
|
||||
type KeyCfg struct {
|
||||
Signing *SigningKeyCfg
|
||||
// more later
|
||||
Signing *SigningKeyCfg
|
||||
Verification []*VerificationKeyCfg
|
||||
}
|
||||
|
||||
// ErrInvalidAlgorithmType represents an invalid algorithm error.
|
||||
|
|
@ -430,6 +430,35 @@ func loadOrCreateAsymmetricKey(keyPath, algorithm string) (any, error) {
|
|||
return loadPrivateKey(keyPath)
|
||||
}
|
||||
|
||||
// save the public key for a private key
|
||||
func savePublicKey(keyPath string, signingKey SigningKey) error {
|
||||
if signingKey.IsSymmetric() {
|
||||
return fmt.Errorf("Saving symmatric key deliberately not supported (path: \"%s\")", keyPath)
|
||||
}
|
||||
bytes, err := x509.MarshalPKIXPublicKey(signingKey.VerifyKey())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publicKeyPEM := &pem.Block{Type: "PUBLIC KEY", Bytes: bytes}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(keyPath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err = f.Close(); err != nil {
|
||||
log.Error("Close: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return pem.Encode(f, publicKeyPEM)
|
||||
}
|
||||
|
||||
// InitSigningKey creates a signing key from SigningKeyCfg
|
||||
// cfgP is set to nil to mark that is has been processed
|
||||
func InitSigningKey(cfgP **SigningKeyCfg) (SigningKey, error) {
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/generate"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testSignVerify(t *testing.T, signKey, verifyKey SigningKey) {
|
||||
func testSignVerify(t *testing.T, signKey SigningKey, verifyKey VerificationKey) {
|
||||
t.Helper()
|
||||
// test sign and verify
|
||||
claimsIn := jwt.RegisteredClaims{
|
||||
|
|
@ -34,9 +36,15 @@ func testSignVerify(t *testing.T, signKey, verifyKey SigningKey) {
|
|||
assert.NotNil(t, valToken.Method)
|
||||
assert.Equal(t, signKey.SigningMethod().Alg(), valToken.Method.Alg())
|
||||
assert.Equal(t, verifyKey.SigningMethod().Alg(), valToken.Method.Alg())
|
||||
|
||||
// asymmetric keys generate JWT with a kid, symmetric not
|
||||
kid, ok := valToken.Header["kid"]
|
||||
assert.True(t, ok)
|
||||
assert.NotNil(t, kid)
|
||||
if signKey.IsSymmetric() {
|
||||
assert.False(t, ok)
|
||||
} else {
|
||||
assert.True(t, ok)
|
||||
assert.NotNil(t, kid)
|
||||
}
|
||||
|
||||
return verifyKey.VerifyKey(), nil
|
||||
})
|
||||
|
|
@ -68,18 +76,53 @@ func TestLoadOrCreateAsymmetricKey(t *testing.T) {
|
|||
useKey := func(t *testing.T, keyPath, algorithm string) {
|
||||
t.Helper()
|
||||
// duplicates loadKey() to some extent, but uses SigningKey
|
||||
cfg := &SigningKeyCfg{
|
||||
assert.NotEmpty(t, keyPath)
|
||||
scfg := &SigningKeyCfg{
|
||||
Algorithm: algorithm,
|
||||
PrivateKeyPath: &keyPath,
|
||||
}
|
||||
|
||||
key, err := InitSigningKey(&cfg)
|
||||
// load the signing key via the settings interface
|
||||
key, err := InitSigningKey(&scfg)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, key)
|
||||
assert.Nil(t, cfg)
|
||||
assert.Nil(t, scfg)
|
||||
assert.NotEmpty(t, key.ID())
|
||||
assert.Nil(t, scfg)
|
||||
|
||||
testSignVerify(t, key, key)
|
||||
|
||||
// load the signing key file as a verification key
|
||||
assert.NotEmpty(t, keyPath)
|
||||
vcfg := &VerificationKeyCfg{
|
||||
Algorithm: algorithm,
|
||||
PublicKeyPath: &keyPath,
|
||||
}
|
||||
|
||||
// load the verification key via the settings interface
|
||||
vkey, err := InitVerificationKey(&vcfg)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, key)
|
||||
assert.Equal(t, key.ID(), vkey.ID())
|
||||
assert.Nil(t, scfg)
|
||||
|
||||
testSignVerify(t, key, vkey)
|
||||
|
||||
// save the public key, then load it and test
|
||||
pKeyPath := keyPath + ".pub"
|
||||
err = savePublicKey(pKeyPath, key)
|
||||
require.NoError(t, err)
|
||||
vcfg = &VerificationKeyCfg{
|
||||
Algorithm: algorithm,
|
||||
PublicKeyPath: &pKeyPath,
|
||||
}
|
||||
vkey, err = InitVerificationKey(&vcfg)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, vkey)
|
||||
assert.Equal(t, key.ID(), vkey.ID())
|
||||
assert.Nil(t, vcfg)
|
||||
|
||||
testSignVerify(t, key, vkey)
|
||||
}
|
||||
t.Run("RSA-2048", func(t *testing.T) {
|
||||
keyPath := filepath.Join(t.TempDir(), "jwt-rsa-2048.priv")
|
||||
|
|
@ -185,3 +228,39 @@ func TestCannotCreatePrivateKey(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Error generating private key")
|
||||
}
|
||||
|
||||
// test symmetic algorithms used via the SigningKey and VerificationKey
|
||||
// interfaces
|
||||
func TestSymmetricKey(t *testing.T) {
|
||||
algorithms := []string{"HS256", "HS384", "HS512"}
|
||||
for _, algorithm := range algorithms {
|
||||
t.Run(algorithm, func(t *testing.T) {
|
||||
secret, _ := generate.NewJwtSecret()
|
||||
assert.NotEmpty(t, secret)
|
||||
|
||||
// init the signing key via the settings interface
|
||||
scfg := &SigningKeyCfg{
|
||||
Algorithm: algorithm,
|
||||
SecretBytes: &secret,
|
||||
}
|
||||
skey, err := InitSigningKey(&scfg)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, skey)
|
||||
assert.Nil(t, scfg)
|
||||
|
||||
testSignVerify(t, skey, skey)
|
||||
|
||||
// init the same key as a VerificationKey
|
||||
vcfg := &VerificationKeyCfg{
|
||||
Algorithm: algorithm,
|
||||
SecretBytes: &secret,
|
||||
}
|
||||
vkey, err := InitVerificationKey(&vcfg)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, vkey)
|
||||
assert.Nil(t, vcfg)
|
||||
|
||||
testSignVerify(t, skey, vkey)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
262
modules/jwtx/verificationkey.go
Normal file
262
modules/jwtx/verificationkey.go
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package jwtx
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"forgejo.org/modules/util"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// The ...KeyCfg types are only used for handover from setting to signingkey
|
||||
// see comment in setting/security.go
|
||||
|
||||
type VerificationKeyCfg struct {
|
||||
Algorithm string
|
||||
SecretBytes *[]byte
|
||||
PublicKeyPath *string
|
||||
}
|
||||
|
||||
// VerificationKey represents a algorithm/key pair to sign JWTs
|
||||
type VerificationKey interface {
|
||||
IsSymmetric() bool
|
||||
SigningMethod() jwt.SigningMethod
|
||||
VerifyKey() any
|
||||
ID() string
|
||||
// want? ToJWK() (map[string]string, error)
|
||||
}
|
||||
|
||||
type hmacVerificationKey struct {
|
||||
signingMethod jwt.SigningMethod
|
||||
secret []byte
|
||||
}
|
||||
|
||||
func (key hmacVerificationKey) IsSymmetric() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (key hmacVerificationKey) SigningMethod() jwt.SigningMethod {
|
||||
return key.signingMethod
|
||||
}
|
||||
|
||||
func (key hmacVerificationKey) VerifyKey() any {
|
||||
return key.secret
|
||||
}
|
||||
|
||||
func (key hmacVerificationKey) ID() string { return "" }
|
||||
|
||||
type rsaVerificationKey struct {
|
||||
signingMethod jwt.SigningMethod
|
||||
key *rsa.PublicKey
|
||||
id string
|
||||
}
|
||||
|
||||
func newRSAVerificationKey(signingMethod jwt.SigningMethod, key *rsa.PublicKey) (rsaVerificationKey, error) {
|
||||
kid, err := util.CreatePublicKeyFingerprint(key)
|
||||
if err != nil {
|
||||
return rsaVerificationKey{}, err
|
||||
}
|
||||
|
||||
return rsaVerificationKey{
|
||||
signingMethod,
|
||||
key,
|
||||
base64.RawURLEncoding.EncodeToString(kid),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (key rsaVerificationKey) IsSymmetric() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (key rsaVerificationKey) SigningMethod() jwt.SigningMethod {
|
||||
return key.signingMethod
|
||||
}
|
||||
|
||||
func (key rsaVerificationKey) VerifyKey() any {
|
||||
return key.key
|
||||
}
|
||||
|
||||
func (key rsaVerificationKey) ID() string {
|
||||
return key.id
|
||||
}
|
||||
|
||||
type eddsaVerificationKey struct {
|
||||
signingMethod jwt.SigningMethod
|
||||
key ed25519.PublicKey
|
||||
id string
|
||||
}
|
||||
|
||||
func newEdDSAVerificationKey(signingMethod jwt.SigningMethod, key ed25519.PublicKey) (eddsaVerificationKey, error) {
|
||||
kid, err := util.CreatePublicKeyFingerprint(key)
|
||||
if err != nil {
|
||||
return eddsaVerificationKey{}, err
|
||||
}
|
||||
|
||||
return eddsaVerificationKey{
|
||||
signingMethod,
|
||||
key,
|
||||
base64.RawURLEncoding.EncodeToString(kid),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (key eddsaVerificationKey) IsSymmetric() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (key eddsaVerificationKey) SigningMethod() jwt.SigningMethod {
|
||||
return key.signingMethod
|
||||
}
|
||||
|
||||
func (key eddsaVerificationKey) VerifyKey() any {
|
||||
return key.key
|
||||
}
|
||||
|
||||
func (key eddsaVerificationKey) ID() string {
|
||||
return key.id
|
||||
}
|
||||
|
||||
type ecdsaVerificationKey struct {
|
||||
signingMethod jwt.SigningMethod
|
||||
key *ecdsa.PublicKey
|
||||
id string
|
||||
}
|
||||
|
||||
func newECDSAVerificationKey(signingMethod jwt.SigningMethod, key *ecdsa.PublicKey) (ecdsaVerificationKey, error) {
|
||||
kid, err := util.CreatePublicKeyFingerprint(key)
|
||||
if err != nil {
|
||||
return ecdsaVerificationKey{}, err
|
||||
}
|
||||
|
||||
return ecdsaVerificationKey{
|
||||
signingMethod,
|
||||
key,
|
||||
base64.RawURLEncoding.EncodeToString(kid),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (key ecdsaVerificationKey) IsSymmetric() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (key ecdsaVerificationKey) SigningMethod() jwt.SigningMethod {
|
||||
return key.signingMethod
|
||||
}
|
||||
|
||||
func (key ecdsaVerificationKey) VerifyKey() any {
|
||||
return key.key
|
||||
}
|
||||
|
||||
func (key ecdsaVerificationKey) ID() string {
|
||||
return key.id
|
||||
}
|
||||
|
||||
// CreateVerificationKey creates a signing key from an algorithm / key pair.
|
||||
func CreateVerificationKey(algorithm string, key any) (VerificationKey, error) {
|
||||
signingMethod := GetSigningMethod(algorithm)
|
||||
if signingMethod == nil {
|
||||
return nil, ErrInvalidAlgorithmType{algorithm}
|
||||
}
|
||||
|
||||
switch signingMethod.(type) {
|
||||
case *jwt.SigningMethodEd25519:
|
||||
publicKey, ok := key.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return nil, jwt.ErrInvalidKeyType
|
||||
}
|
||||
return newEdDSAVerificationKey(signingMethod, publicKey)
|
||||
case *jwt.SigningMethodECDSA:
|
||||
publicKey, ok := key.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, jwt.ErrInvalidKeyType
|
||||
}
|
||||
return newECDSAVerificationKey(signingMethod, publicKey)
|
||||
case *jwt.SigningMethodRSA:
|
||||
publicKey, ok := key.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, jwt.ErrInvalidKeyType
|
||||
}
|
||||
return newRSAVerificationKey(signingMethod, publicKey)
|
||||
default:
|
||||
secret, ok := key.([]byte)
|
||||
if !ok {
|
||||
return nil, jwt.ErrInvalidKeyType
|
||||
}
|
||||
return hmacVerificationKey{signingMethod, secret}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// loadPublicKey loads a public key
|
||||
func loadPublicKey(keyPath string) (any, error) {
|
||||
bytes, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(bytes)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("no valid PEM data found in %s", keyPath)
|
||||
} else if block.Type != "PUBLIC KEY" {
|
||||
return nil, fmt.Errorf("expected PUBLIC KEY, got %s in %s", block.Type, keyPath)
|
||||
}
|
||||
|
||||
return x509.ParsePKIXPublicKey(block.Bytes)
|
||||
}
|
||||
|
||||
// InitVerificationKey creates a signing key from VerificationKeyCfg
|
||||
// cfgP is set to nil to mark that is has been processed
|
||||
func InitVerificationKey(cfgP **VerificationKeyCfg) (VerificationKey, error) {
|
||||
cfg := *cfgP
|
||||
*cfgP = nil
|
||||
var err error
|
||||
var key VerificationKey
|
||||
|
||||
if IsValidSymmetricAlgorithm(cfg.Algorithm) {
|
||||
key, err = CreateVerificationKey(cfg.Algorithm, *cfg.SecretBytes)
|
||||
} else if IsValidAsymmetricAlgorithm(cfg.Algorithm) {
|
||||
key, err = InitAsymmetricVerificationKey(*cfg.PublicKeyPath, cfg.Algorithm)
|
||||
} else {
|
||||
// should never happen, setting.loadVerificationKeyCfg() ensures
|
||||
err = ErrInvalidAlgorithmType{Algorithm: cfg.Algorithm}
|
||||
}
|
||||
|
||||
return key, err
|
||||
}
|
||||
|
||||
// InitAsymmetricVerificationKey creates an asymmetric signing key from settings or creates a random key.
|
||||
func InitAsymmetricVerificationKey(keyPath, algorithm string) (VerificationKey, error) {
|
||||
var err error
|
||||
var key any
|
||||
|
||||
if !IsValidAsymmetricAlgorithm(algorithm) {
|
||||
return nil, ErrInvalidAlgorithmType{Algorithm: algorithm}
|
||||
}
|
||||
|
||||
key, err = loadPublicKey(keyPath)
|
||||
if err == nil {
|
||||
return CreateVerificationKey(algorithm, key)
|
||||
}
|
||||
|
||||
// We deliberately support loading a signing key as a verification key
|
||||
// such that users can just move a private key to another location to use it
|
||||
// as a verification key
|
||||
key, err2 := loadPrivateKey(keyPath)
|
||||
if err2 != nil {
|
||||
return nil, fmt.Errorf("Neither public key nor private key: %w/%w", err, err2)
|
||||
}
|
||||
|
||||
signingKey, err2 := CreateSigningKey(algorithm, key)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
return signingKey.(VerificationKey), nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue