Skip to content

Commit

Permalink
HMS-2694 feat: Host conf token signing
Browse files Browse the repository at this point in the history
Add helpers to build a host configuration token and to sign a token with
multiple keys.

`BuildHostconfToken()` builds a new token and returns a token instance.

`SignToken()` signs a token with multiple keys and returns a JSON
string.

Signed-off-by: Christian Heimes <cheimes@redhat.com>
  • Loading branch information
tiran authored and frasertweedale committed Oct 10, 2023
1 parent ad34162 commit 730b8ec
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 8 deletions.
72 changes: 72 additions & 0 deletions internal/infrastructure/token/hostconf_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package token

import (
"crypto/rand"
"encoding/base64"

"time"

"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/podengo-project/idmsvc-backend/internal/api/public"
)

// BuildHostconfToken creates a token instance with all claims for
// host configuration token.
func BuildHostconfToken(
rhsmId public.SubscriptionManagerId,
orgId string,
inventoryId public.HostId,
fqdn public.Fqdn,
domainId public.DomainId,
validity time.Duration,
) (tok jwt.Token, err error) {
// random JTI
r := make([]byte, 6)
if _, err = rand.Read(r); err != nil {
return nil, err
}
jti := base64.RawURLEncoding.EncodeToString(r)

now := time.Now()
return jwt.NewBuilder().
Issuer(TokenIssuer).
Subject(rhsmId.String()).
Audience([]string{AudJoinHost}).
JwtID(jti).
IssuedAt(now).
NotBefore(now).
Expiration(now.Add(validity)).
Claim(ClaimOrgId, orgId).
Claim(ClaimInventoryId, inventoryId.String()).
Claim(ClaimFqdn, string(fqdn)).
Claim(ClaimDomainId, domainId.String()).
Build()
}

// signToken serializes the token and signs it with all given keys. The return
// value is a JWS in JSON format.
func SignToken(tok jwt.Token, keys []jwk.Key) ([]byte, error) {
serialized, err := jwt.NewSerializer().Serialize(tok)
if err != nil {
return nil, err
}

opts := []jws.SignOption{}
// sign with all keys
for _, key := range keys {
opts = append(opts, jws.WithKey(key.Algorithm(), key))
}
// always return JSON format (non-compact serialization)
opts = append(opts, jws.WithJSON())
return jws.Sign(serialized, opts...)
}

func init() {
var s string
jwt.RegisterCustomField(ClaimDomainId, s)
jwt.RegisterCustomField(ClaimFqdn, s)
jwt.RegisterCustomField(ClaimInventoryId, s)
jwt.RegisterCustomField(ClaimOrgId, s)
}
94 changes: 94 additions & 0 deletions internal/infrastructure/token/hostconf_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package token

import (
"testing"
"time"

"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/podengo-project/idmsvc-backend/internal/test"
"github.com/stretchr/testify/assert"
)

func testToken() (jwt.Token, error) {
return BuildHostconfToken(
test.Server1.CertUUID,
test.OrgId,
test.Server1.InventoryUUID,
test.Server1.Fqdn,
test.DomainUUID,
time.Hour,
)
}

func TestBuildToken(t *testing.T) {
tok, err := testToken()
assert.NoError(t, err)

assert.Equal(t, tok.Issuer(), TokenIssuer)
assert.Equal(t, tok.Subject(), test.Server1.CertCN)
assert.Equal(t, tok.Audience(), []string{AudJoinHost})
assert.NotEmpty(t, tok.JwtID())
iat := tok.IssuedAt()
assert.Equal(t, iat, tok.NotBefore())
assert.Equal(t, iat.Add(time.Hour), tok.Expiration())

ifc, ok := tok.Get(ClaimOrgId)
assert.True(t, ok)
assert.Equal(t, ifc.(string), test.OrgId)

ifc, ok = tok.Get(ClaimInventoryId)
assert.True(t, ok)
assert.Equal(t, ifc.(string), test.Server1.InventoryId)

ifc, ok = tok.Get(ClaimFqdn)
assert.True(t, ok)
assert.Equal(t, ifc.(string), test.Server1.Fqdn)

ifc, ok = tok.Get(ClaimDomainId)
assert.True(t, ok)
assert.Equal(t, ifc.(string), test.DomainId)
}

func TestSignToken(t *testing.T) {
tok, err := testToken()
assert.NoError(t, err)

exp := time.Now().Add(time.Hour)
priv1, err := GeneratePrivateJWK(exp)
assert.NoError(t, err)
pub1, err := priv1.PublicKey()
assert.NoError(t, err)

priv2, err := GeneratePrivateJWK(exp)
assert.NoError(t, err)
pub2, err := priv2.PublicKey()
assert.NoError(t, err)

privs := []jwk.Key{priv1, priv2}
pubs := []jwk.Key{pub1, pub2}

sig, err := SignToken(tok, privs)
assert.NoError(t, err)

set := jwk.NewSet()
for _, pub := range pubs {
err = set.AddKey(pub)
assert.NoError(t, err)
}
verified, err := jws.Verify(sig, jws.WithKeySet(set))
assert.NoError(t, err)

toks, err := jwt.NewSerializer().Serialize(tok)
assert.NoError(t, err)
assert.Equal(t, verified, toks)

verified, err = jws.Verify(sig, jws.WithKey(priv1.Algorithm(), pub1))
assert.NoError(t, err)
assert.Equal(t, verified, toks)

verified, err = jws.Verify(sig, jws.WithKey(priv2.Algorithm(), pub2))
assert.NoError(t, err)
assert.Equal(t, verified, toks)
}
8 changes: 0 additions & 8 deletions internal/infrastructure/token/jwk.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,6 @@ import (
"github.com/lestrrat-go/jwx/v2/jwk"
)

type KeyState int

const (
ValidKey KeyState = iota
ExpiredKey
InvalidKey
)

const KeyCurve = jwa.P256

// Generate a private key with additional properties
Expand Down
21 changes: 21 additions & 0 deletions internal/infrastructure/token/variables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package token

// JWK key state
type KeyState int

const (
ValidKey KeyState = iota
ExpiredKey
InvalidKey
RevokedKey
)

// Token
const (
TokenIssuer = "idmsvc/v1"
AudJoinHost = "join host"
ClaimOrgId = "rhorg"
ClaimDomainId = "rhdomid"
ClaimFqdn = "rhfqdn"
ClaimInventoryId = "rhinvid"
)

0 comments on commit 730b8ec

Please sign in to comment.