Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Unit tests

on:
pull_request:
types: [opened, ready_for_review]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Install just
uses: taiki-e/install-action@just

- name: Run tests
run: just test
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ go 1.24.3

require (
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/lestrrat-go/jwx v1.2.31
github.com/stretchr/testify v1.11.1
k8s.io/api v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/client-go v0.34.1
Expand All @@ -12,18 +14,25 @@ require (

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.3 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
Expand All @@ -34,6 +43,7 @@ require (
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
Expand Down
24 changes: 22 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
Expand All @@ -18,6 +20,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
Expand All @@ -44,6 +48,19 @@ 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/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs=
github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.31 h1:/OM9oNl/fzyldpv5HKZ9m7bTywa7COUfg8gujd9nJ54=
github.com/lestrrat-go/jwx v1.2.31/go.mod h1:eQJKoRwWcLg4PfD5CFA5gIZGxhPgoPYq9pZISdxLf0c=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down Expand Up @@ -72,11 +89,12 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand All @@ -90,6 +108,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
Expand Down
49 changes: 16 additions & 33 deletions k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,18 @@ import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math/big"
"net/http"
"os"
"strings"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/jsnctl/kubespiffe/pkg/generated/clientset/versioned"
"github.com/lestrrat-go/jwx/jwk"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
Expand Down Expand Up @@ -103,12 +101,12 @@ func loadCertPool(path string) (*x509.CertPool, error) {
return pool, nil
}

func extractBearer(header string) string {
prefix := "Bearer "
if len(header) > len(prefix) && header[:len(prefix)] == prefix {
return header[len(prefix):]
func extractBearerToken(header string) string {
hasToken := strings.HasPrefix(header, "Bearer ")
if !hasToken {
return ""
}
return ""
return strings.TrimPrefix(header, "Bearer ")
}

func verifyPSAT(psat string, jwks *JWKS) (map[string]any, error) {
Expand Down Expand Up @@ -182,38 +180,23 @@ func findKeyByKID(jwks *JWKS, kid string) (map[string]interface{}, error) {
return nil, fmt.Errorf("no key found for kid: %s", kid)
}

func jwkToPublicKey(jwk map[string]interface{}) (*rsa.PublicKey, error) {
// TODO: This can definitely be simplified with the Go JWT
// library API, this fn was original provided by Gemini and is likely
// to be over-engineered and potentially buggy
nStr, okN := jwk["n"].(string)
eStr, okE := jwk["e"].(string)
if !okN || !okE {
return nil, errors.New("missing n or e in jwk")
}

nBytes, err := base64.RawURLEncoding.DecodeString(nStr)
func jwkToPublicKey(keyMap map[string]interface{}) (*rsa.PublicKey, error) {
keyData, err := json.Marshal(keyMap)
if err != nil {
return nil, fmt.Errorf("decode n: %w", err)
return nil, fmt.Errorf("problem marshaling JWK: %w", err)
}
eBytes, err := base64.RawURLEncoding.DecodeString(eStr)

key, err := jwk.ParseKey(keyData)
if err != nil {
return nil, fmt.Errorf("decode e: %w", err)
return nil, fmt.Errorf("problem with parsing JWK: %w", err)
}

var e int
switch len(eBytes) {
case 3:
e = int(binary.BigEndian.Uint32(append([]byte{0}, eBytes...)))
default:
e = int(binary.BigEndian.Uint16(eBytes))
var publicKey rsa.PublicKey
if err := key.Raw(&publicKey); err != nil {
return nil, fmt.Errorf("problem extracting key: %w", err)
}

pub := &rsa.PublicKey{
N: new(big.Int).SetBytes(nBytes),
E: e,
}
return pub, nil
return &publicKey, nil
}

type KubernetesWorkloadClaims struct {
Expand Down
120 changes: 120 additions & 0 deletions k8s_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package main

import (
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"math/big"
"testing"

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

func Test_extractBearer(t *testing.T) {
tests := []struct {
name string
header string
want string
}{
{
name: "valid bearer token",
header: "Bearer i-am-a-bearer-token",
want: "i-am-a-bearer-token",
},
{
name: "missing prefix",
header: "i-might-be-a-bearer-token-but-i-have-no-Bearer-before-me",
want: "",
},
{
name: "empty header",
header: "",
want: "",
},
{
name: "prefix only",
header: "Bearer ",
want: "",
},
{
name: "case sensitive prefix",
header: "bearer i-could-be-a-bearer-token-but-the-guy-before-me-ruined-it",
want: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractBearerToken(tt.header)
assert.Equal(t, got, tt.want)
})
}
}

func mockRSAJWK() map[string]any {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil
}

nStr := base64.RawURLEncoding.EncodeToString(privateKey.PublicKey.N.Bytes())

eBytes := big.NewInt(int64(privateKey.PublicKey.E)).Bytes()
eStr := base64.RawURLEncoding.EncodeToString(eBytes)

return map[string]any{
"kty": "RSA",
"n": nStr,
"e": eStr,
}
}

func Test_jwkToPublicKey(t *testing.T) {
validJWK := mockRSAJWK()

tests := []struct {
name string
jwk map[string]any
wantErr bool
}{
{
name: "valid JWK",
jwk: validJWK,
wantErr: false,
},
{
name: "missing n",
jwk: map[string]any{
"e": validJWK["e"],
},
wantErr: true,
},
{
name: "missing e",
jwk: map[string]any{
"n": validJWK["n"],
},
wantErr: true,
},
{
name: "invalid base64",
jwk: map[string]any{
"n": "inv@lid",
"e": validJWK["e"],
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := jwkToPublicKey(tt.jwk)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.IsType(t, &rsa.PublicKey{}, got)
})
}
}
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func main() {
log.Fatalf("problem with kubespiffe clientset: %v", err)
}
http.HandleFunc("/v1/svid", func(w http.ResponseWriter, r *http.Request) {
token := extractBearer(r.Header.Get("Authorization"))
token := extractBearerToken(r.Header.Get("Authorization"))
if token == "" {
http.Error(w, "missing bearer token", http.StatusUnauthorized)
return
Expand Down