forked from TrueCloudLab/distribution
Merge pull request #1686 from marcusmartins/nbf-leeway
Add leeway to JWT nbf checking
This commit is contained in:
commit
dd355e95af
2 changed files with 92 additions and 11 deletions
|
@ -20,6 +20,9 @@ const (
|
|||
// TokenSeparator is the value which separates the header, claims, and
|
||||
// signature in the compact serialization of a JSON Web Token.
|
||||
TokenSeparator = "."
|
||||
// Leeway is the Duration that will be added to NBF and EXP claim
|
||||
// checks to account for clock skew as per https://tools.ietf.org/html/rfc7519#section-4.1.5
|
||||
Leeway = 60 * time.Second
|
||||
)
|
||||
|
||||
// Errors used by token parsing and verification.
|
||||
|
@ -143,9 +146,17 @@ func (t *Token) Verify(verifyOpts VerifyOptions) error {
|
|||
}
|
||||
|
||||
// Verify that the token is currently usable and not expired.
|
||||
currentUnixTime := time.Now().Unix()
|
||||
if !(t.Claims.NotBefore <= currentUnixTime && currentUnixTime <= t.Claims.Expiration) {
|
||||
log.Errorf("token not to be used before %d or after %d - currently %d", t.Claims.NotBefore, t.Claims.Expiration, currentUnixTime)
|
||||
currentTime := time.Now()
|
||||
|
||||
ExpWithLeeway := time.Unix(t.Claims.Expiration, 0).Add(Leeway)
|
||||
if currentTime.After(ExpWithLeeway) {
|
||||
log.Errorf("token not to be used after %s - currently %s", ExpWithLeeway, currentTime)
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
NotBeforeWithLeeway := time.Unix(t.Claims.NotBefore, 0).Add(-Leeway)
|
||||
if currentTime.Before(NotBeforeWithLeeway) {
|
||||
log.Errorf("token not to be used before %s - currently %s", NotBeforeWithLeeway, currentTime)
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ func makeTrustedKeyMap(rootKeys []libtrust.PrivateKey) map[string]libtrust.Publi
|
|||
return trustedKeys
|
||||
}
|
||||
|
||||
func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey libtrust.PrivateKey, depth int) (*Token, error) {
|
||||
func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey libtrust.PrivateKey, depth int, now time.Time, exp time.Time) (*Token, error) {
|
||||
signingKey, err := makeSigningKeyWithChain(rootKey, depth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to make signing key with chain: %s", err)
|
||||
|
@ -109,8 +109,6 @@ func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey l
|
|||
RawJWK: &rawJWK,
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
randomBytes := make([]byte, 15)
|
||||
if _, err = rand.Read(randomBytes); err != nil {
|
||||
return nil, fmt.Errorf("unable to read random bytes for jwt id: %s", err)
|
||||
|
@ -120,7 +118,7 @@ func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey l
|
|||
Issuer: issuer,
|
||||
Subject: "foo",
|
||||
Audience: audience,
|
||||
Expiration: now.Add(5 * time.Minute).Unix(),
|
||||
Expiration: exp.Unix(),
|
||||
NotBefore: now.Unix(),
|
||||
IssuedAt: now.Unix(),
|
||||
JWTID: base64.URLEncoding.EncodeToString(randomBytes),
|
||||
|
@ -188,7 +186,7 @@ func TestTokenVerify(t *testing.T) {
|
|||
tokens := make([]*Token, 0, numTokens)
|
||||
|
||||
for i := 0; i < numTokens; i++ {
|
||||
token, err := makeTestToken(issuer, audience, access, rootKeys[i], i)
|
||||
token, err := makeTestToken(issuer, audience, access, rootKeys[i], i, time.Now(), time.Now().Add(5*time.Minute))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -209,6 +207,78 @@ func TestTokenVerify(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// This tests that we don't fail tokens with nbf within
|
||||
// the defined leeway in seconds
|
||||
func TestLeeway(t *testing.T) {
|
||||
var (
|
||||
issuer = "test-issuer"
|
||||
audience = "test-audience"
|
||||
access = []*ResourceActions{
|
||||
{
|
||||
Type: "repository",
|
||||
Name: "foo/bar",
|
||||
Actions: []string{"pull", "push"},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
rootKeys, err := makeRootKeys(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
trustedKeys := makeTrustedKeyMap(rootKeys)
|
||||
|
||||
verifyOps := VerifyOptions{
|
||||
TrustedIssuers: []string{issuer},
|
||||
AcceptedAudiences: []string{audience},
|
||||
Roots: nil,
|
||||
TrustedKeys: trustedKeys,
|
||||
}
|
||||
|
||||
// nbf verification should pass within leeway
|
||||
futureNow := time.Now().Add(time.Duration(5) * time.Second)
|
||||
token, err := makeTestToken(issuer, audience, access, rootKeys[0], 0, futureNow, futureNow.Add(5*time.Minute))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := token.Verify(verifyOps); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// nbf verification should fail with a skew larger than leeway
|
||||
futureNow = time.Now().Add(time.Duration(61) * time.Second)
|
||||
token, err = makeTestToken(issuer, audience, access, rootKeys[0], 0, futureNow, futureNow.Add(5*time.Minute))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = token.Verify(verifyOps); err == nil {
|
||||
t.Fatal("Verification should fail for token with nbf in the future outside leeway")
|
||||
}
|
||||
|
||||
// exp verification should pass within leeway
|
||||
token, err = makeTestToken(issuer, audience, access, rootKeys[0], 0, time.Now(), time.Now().Add(-59*time.Second))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = token.Verify(verifyOps); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// exp verification should fail with a skew larger than leeway
|
||||
token, err = makeTestToken(issuer, audience, access, rootKeys[0], 0, time.Now(), time.Now().Add(-60*time.Second))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = token.Verify(verifyOps); err == nil {
|
||||
t.Fatal("Verification should fail for token with exp in the future outside leeway")
|
||||
}
|
||||
}
|
||||
|
||||
func writeTempRootCerts(rootKeys []libtrust.PrivateKey) (filename string, err error) {
|
||||
rootCerts, err := makeRootCerts(rootKeys)
|
||||
if err != nil {
|
||||
|
@ -307,7 +377,7 @@ func TestAccessController(t *testing.T) {
|
|||
Name: testAccess.Name,
|
||||
Actions: []string{testAccess.Action},
|
||||
}},
|
||||
rootKeys[1], 1, // Everything is valid except the key which signed it.
|
||||
rootKeys[1], 1, time.Now(), time.Now().Add(5*time.Minute), // Everything is valid except the key which signed it.
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -333,7 +403,7 @@ func TestAccessController(t *testing.T) {
|
|||
token, err = makeTestToken(
|
||||
issuer, service,
|
||||
[]*ResourceActions{}, // No access specified.
|
||||
rootKeys[0], 1,
|
||||
rootKeys[0], 1, time.Now(), time.Now().Add(5*time.Minute),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -363,7 +433,7 @@ func TestAccessController(t *testing.T) {
|
|||
Name: testAccess.Name,
|
||||
Actions: []string{testAccess.Action},
|
||||
}},
|
||||
rootKeys[0], 1,
|
||||
rootKeys[0], 1, time.Now(), time.Now().Add(5*time.Minute),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
Loading…
Reference in a new issue