Repository: lestrrat-go/jwx
Pull request #782 (2022-07-19 05:12:25)

merge develop/v2 into v2


Patch:
diff --git a/Changes b/Changes
index 9a7fd71d..786324aa 100644
--- a/Changes
+++ b/Changes
@@ -4,6 +4,36 @@ Changes
 v2 has many incompatibilities with v1. To see the full list of differences between
 v1 and v2, please read the Changes-v2.md file (https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes-v2.md)
 
+v2.0.4 - 19 Jul 2022
+[Bug Fixes]
+  * [jwk] github.com/lestrrat-go/httprc, which jwk.Cache depends on,
+    had a problem with inserting URLs to be re-fetched into its queue.
+    As a result it could have been the case that some JWKS were not
+    updated properly. Please upgrade if you use jwk.Cache.
+
+  * [jwk] cert.Get could fail with an out of bounds index look up
+
+  * [jwk] Fix doc buglet in `KeyType()` method
+
+[New Features]
+  * [jws] Add `jws.WithMultipleKeysPerKeyID()` sub-option to allow non-unique
+    key IDs in a given JWK set. By default we assume that a key ID is unique
+    within a key set, but enabling this option allows you to handle JWK sets
+    that contain multiple keys that contain the same key ID.
+
+  * [jwt] Before v2.0.1, sub-second accuracy for time based fields
+    (i.e. `iat`, `exp`, `nbf`) were not respected. Because of this the code
+    to evaluate this code had always truncated any-subsecond portion
+    of these fields, and therefore no sub-second comparisons worked.
+    A new option for validation `jwt.WithTruncation()` has been added
+    to workaround this. This option controls the value used to truncate
+    the time fields. When set to 0, sub-second comparison would be
+    possible.
+    FIY, truncatation will still happen because we do not want to
+    use the monotonic clocks when making comparisons. It's just that
+    truncating using `0` as its argument effectively only strips out
+    the monotonic clock
+
 v2.0.3 - 13 Jun 2022
 [Bug Fixes]
   * [jwk] Update dependency on github.com/lestrrat-go/httprc to v1.0.2 to
diff --git a/bench/performance/go.sum b/bench/performance/go.sum
index f5787cdb..704635e8 100644
--- a/bench/performance/go.sum
+++ b/bench/performance/go.sum
@@ -1,16 +1,17 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 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/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
-github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
-github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
+github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
 github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
 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/httprc v1.0.2 h1:1hPZNeBRt9cvvYFR83XTkoQQidSFmAj3dqgR3AuQj9U=
-github.com/lestrrat-go/httprc v1.0.2/go.mod h1:5Ml+nB++j6IC0e6LzefJnrpMQDKgDwDCaIQQzhbqhJM=
+github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
+github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
 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/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
@@ -18,10 +19,11 @@ github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmt
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 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.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
-github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
diff --git a/cert/chain.go b/cert/chain.go
index 9396ce97..0c4746fb 100644
--- a/cert/chain.go
+++ b/cert/chain.go
@@ -48,7 +48,7 @@ func (cc *Chain) UnmarshalJSON(data []byte) error {
 // stored. `false` will be returned in the second argument if
 // the corresponding index is out of range.
 func (cc *Chain) Get(index int) ([]byte, bool) {
-	if index < 0 || index > len(cc.certificates) {
+	if index < 0 || index >= len(cc.certificates) {
 		return nil, false
 	}
 
diff --git a/cmd/jwx/go.mod b/cmd/jwx/go.mod
index 1a782be5..d807c593 100644
--- a/cmd/jwx/go.mod
+++ b/cmd/jwx/go.mod
@@ -11,10 +11,10 @@ require (
 require (
 	github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
-	github.com/goccy/go-json v0.9.7 // indirect
+	github.com/goccy/go-json v0.9.10 // indirect
 	github.com/lestrrat-go/blackmagic v1.0.1 // indirect
 	github.com/lestrrat-go/httpcc v1.0.1 // indirect
-	github.com/lestrrat-go/httprc v1.0.2 // indirect
+	github.com/lestrrat-go/httprc v1.0.4 // indirect
 	github.com/lestrrat-go/iter v1.0.2 // indirect
 	github.com/lestrrat-go/option v1.0.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
diff --git a/cmd/jwx/go.sum b/cmd/jwx/go.sum
index a0222574..89818a5c 100644
--- a/cmd/jwx/go.sum
+++ b/cmd/jwx/go.sum
@@ -2,19 +2,20 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
 github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 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/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
-github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
-github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
+github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
 github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
 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/httprc v1.0.2 h1:1hPZNeBRt9cvvYFR83XTkoQQidSFmAj3dqgR3AuQj9U=
-github.com/lestrrat-go/httprc v1.0.2/go.mod h1:5Ml+nB++j6IC0e6LzefJnrpMQDKgDwDCaIQQzhbqhJM=
+github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
+github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
 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/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
@@ -26,10 +27,11 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 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.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
-github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
diff --git a/examples/go.sum b/examples/go.sum
index 995753a7..a5f866ad 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -1,19 +1,20 @@
 github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY=
 github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 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/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
-github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
-github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
+github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
 github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
 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/httprc v1.0.2 h1:1hPZNeBRt9cvvYFR83XTkoQQidSFmAj3dqgR3AuQj9U=
-github.com/lestrrat-go/httprc v1.0.2/go.mod h1:5Ml+nB++j6IC0e6LzefJnrpMQDKgDwDCaIQQzhbqhJM=
+github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
+github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
 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/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
@@ -21,10 +22,11 @@ github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmt
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 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.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
-github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
diff --git a/go.mod b/go.mod
index 85967c3a..f59a61e8 100644
--- a/go.mod
+++ b/go.mod
@@ -4,11 +4,11 @@ go 1.16
 
 require (
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
-	github.com/goccy/go-json v0.9.7
+	github.com/goccy/go-json v0.9.10
 	github.com/lestrrat-go/blackmagic v1.0.1
-	github.com/lestrrat-go/httprc v1.0.2
+	github.com/lestrrat-go/httprc v1.0.4
 	github.com/lestrrat-go/iter v1.0.2
 	github.com/lestrrat-go/option v1.0.0
-	github.com/stretchr/testify v1.7.2
+	github.com/stretchr/testify v1.8.0
 	golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
 )
diff --git a/go.sum b/go.sum
index f5787cdb..704635e8 100644
--- a/go.sum
+++ b/go.sum
@@ -1,16 +1,17 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 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/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
-github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
-github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
+github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
 github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
 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/httprc v1.0.2 h1:1hPZNeBRt9cvvYFR83XTkoQQidSFmAj3dqgR3AuQj9U=
-github.com/lestrrat-go/httprc v1.0.2/go.mod h1:5Ml+nB++j6IC0e6LzefJnrpMQDKgDwDCaIQQzhbqhJM=
+github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
+github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
 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/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
@@ -18,10 +19,11 @@ github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmt
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 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.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
-github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
diff --git a/jwk/interface_gen.go b/jwk/interface_gen.go
index aa938218..b3bcf31f 100644
--- a/jwk/interface_gen.go
+++ b/jwk/interface_gen.go
@@ -88,7 +88,7 @@ type Key interface {
 	// If the key is already a public key, it returns a new copy minus the disallowed fields as above.
 	PublicKey() (Key, error)
 
-	// KeyType returns the `kid` of a JWK
+	// KeyType returns the `kty` of a JWK
 	KeyType() jwa.KeyType
 	// KeyUsage returns `use` of a JWK
 	KeyUsage() string
diff --git a/jws/key_provider.go b/jws/key_provider.go
index 02fcded8..3bb27c3e 100644
--- a/jws/key_provider.go
+++ b/jws/key_provider.go
@@ -102,10 +102,11 @@ func (kp *staticKeyProvider) FetchKeys(_ context.Context, sink KeySink, _ *Signa
 }
 
 type keySetProvider struct {
-	set            jwk.Set
-	requireKid     bool // true if `kid` must be specified
-	useDefault     bool // true if the first key should be used iff there's exactly one key in set
-	inferAlgorithm bool // true if the algorithm should be inferred from key type
+	set                  jwk.Set
+	requireKid           bool // true if `kid` must be specified
+	useDefault           bool // true if the first key should be used iff there's exactly one key in set
+	inferAlgorithm       bool // true if the algorithm should be inferred from key type
+	multipleKeysPerKeyID bool // true if we should attempt to match multiple keys per key ID. if false we assume that only one key exists for a given key ID
 }
 
 func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, sig *Signature, _ *Message) error {
@@ -151,8 +152,6 @@ func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, sig *Signature, _
 
 func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, sig *Signature, msg *Message) error {
 	if kp.requireKid {
-		var key jwk.Key
-
 		wantedKid := sig.ProtectedHeaders().KeyID()
 		if wantedKid == "" {
 			// If the kid is NOT specified... kp.useDefault needs to be true, and the
@@ -165,19 +164,43 @@ func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, sig *Signat
 
 			// if we got here, then useDefault == true AND there is exactly
 			// one key in the set.
-			key, _ = kp.set.Key(0)
-		} else {
-			// Otherwise we better be able to look up the key, baby.
-			v, ok := kp.set.LookupKeyID(wantedKid)
+			key, _ := kp.set.Key(0)
+			return kp.selectKey(sink, key, sig, msg)
+		}
+
+		// Otherwise we better be able to look up the key.
+		// <= v2.0.3 backwards compatible case: only match a single key
+		// whose key ID matches `wantedKid`
+		if !kp.multipleKeysPerKeyID {
+			key, ok := kp.set.LookupKeyID(wantedKid)
 			if !ok {
 				return fmt.Errorf(`failed to find key with key ID %q in key set`, wantedKid)
 			}
-			key = v
+			return kp.selectKey(sink, key, sig, msg)
 		}
 
-		return kp.selectKey(sink, key, sig, msg)
+		// if multipleKeysPerKeyID is true, we attempt all keys whose key ID matches
+		// the wantedKey
+		var ok bool
+		for i := 0; i < kp.set.Len(); i++ {
+			key, _ := kp.set.Key(i)
+			if key.KeyID() != wantedKid {
+				continue
+			}
+
+			if err := kp.selectKey(sink, key, sig, msg); err != nil {
+				continue
+			}
+			ok = true
+			// continue processing so that we try all keys with the same key ID
+		}
+		if !ok {
+			return fmt.Errorf(`failed to find key with key ID %q in key set`, wantedKid)
+		}
+		return nil
 	}
 
+	// Otherwise just try all keys
 	for i := 0; i < kp.set.Len(); i++ {
 		key, _ := kp.set.Key(i)
 		if err := kp.selectKey(sink, key, sig, msg); err != nil {
diff --git a/jws/options.go b/jws/options.go
index ea5972ea..7b38e92d 100644
--- a/jws/options.go
+++ b/jws/options.go
@@ -119,7 +119,7 @@ func WithKey(alg jwa.KeyAlgorithm, key interface{}, options ...WithKeySuboption)
 // suboption types.
 func WithKeySet(set jwk.Set, options ...WithKeySetSuboption) VerifyOption {
 	requireKid := true
-	var useDefault, inferAlgorithm bool
+	var useDefault, inferAlgorithm, multipleKeysPerKeyID bool
 	for _, option := range options {
 		//nolint:forcetypeassert
 		switch option.Ident() {
@@ -127,16 +127,19 @@ func WithKeySet(set jwk.Set, options ...WithKeySetSuboption) VerifyOption {
 			requireKid = option.Value().(bool)
 		case identUseDefault{}:
 			useDefault = option.Value().(bool)
+		case identMultipleKeysPerKeyID{}:
+			multipleKeysPerKeyID = option.Value().(bool)
 		case identInferAlgorithmFromKey{}:
 			inferAlgorithm = option.Value().(bool)
 		}
 	}
 
 	return WithKeyProvider(&keySetProvider{
-		set:            set,
-		requireKid:     requireKid,
-		useDefault:     useDefault,
-		inferAlgorithm: inferAlgorithm,
+		set:                  set,
+		requireKid:           requireKid,
+		useDefault:           useDefault,
+		multipleKeysPerKeyID: multipleKeysPerKeyID,
+		inferAlgorithm:       inferAlgorithm,
 	})
 }
 
diff --git a/jws/options.yaml b/jws/options.yaml
index 7e2fdfbf..5e1b5b2a 100644
--- a/jws/options.yaml
+++ b/jws/options.yaml
@@ -117,9 +117,18 @@ options:
     interface: WithKeySetSuboption
     argument_type: bool
     comment: |
-      WithrequiredKid specifies whether the keys in the jwk.Set should
+      WithRequiredKid specifies whether the keys in the jwk.Set should
       only be matched if the target JWS message's Key ID and the Key ID
       in the given key matches.
+  - ident: MultipleKeysPerKeyID
+    interface: WithKeySetSuboption
+    argument_type: bool
+    comment: |
+      WithMultipleKeysPerKeyID specifies if we should expect multiple keys
+      to match against a key ID. By default it is assumed that key IDs are
+      unique, i.e. for a given key ID, the key set only contains a single
+      key that has the matching ID. When this option is set to true,
+      multiple keys that match the same key ID in the set can be tried.
   - ident: Pretty
     interface: WithJSONSuboption
     argument_type: bool
diff --git a/jws/options_gen.go b/jws/options_gen.go
index 85874874..fea18bf7 100644
--- a/jws/options_gen.go
+++ b/jws/options_gen.go
@@ -132,6 +132,7 @@ type identKey struct{}
 type identKeyProvider struct{}
 type identKeyUsed struct{}
 type identMessage struct{}
+type identMultipleKeysPerKeyID struct{}
 type identPretty struct{}
 type identProtectedHeaders struct{}
 type identPublicHeaders struct{}
@@ -175,6 +176,10 @@ func (identMessage) String() string {
 	return "WithMessage"
 }
 
+func (identMultipleKeysPerKeyID) String() string {
+	return "WithMultipleKeysPerKeyID"
+}
+
 func (identPretty) String() string {
 	return "WithPretty"
 }
@@ -268,6 +273,15 @@ func WithMessage(v *Message) VerifyOption {
 	return &verifyOption{option.New(identMessage{}, v)}
 }
 
+// WithMultipleKeysPerKeyID specifies if we should expect multiple keys
+// to match against a key ID. By default it is assumed that key IDs are
+// unique, i.e. for a given key ID, the key set only contains a single
+// key that has the matching ID. When this option is set to true,
+// multiple keys that match the same key ID in the set can be tried.
+func WithMultipleKeysPerKeyID(v bool) WithKeySetSuboption {
+	return &withKeySetSuboption{option.New(identMultipleKeysPerKeyID{}, v)}
+}
+
 // WithPretty specifies whether the JSON output should be formatted and
 // indented
 func WithPretty(v bool) WithJSONSuboption {
@@ -293,7 +307,7 @@ func WithPublicHeaders(v Headers) WithKeySuboption {
 	return &withKeySuboption{option.New(identPublicHeaders{}, v)}
 }
 
-// WithrequiredKid specifies whether the keys in the jwk.Set should
+// WithRequiredKid specifies whether the keys in the jwk.Set should
 // only be matched if the target JWS message's Key ID and the Key ID
 // in the given key matches.
 func WithRequireKid(v bool) WithKeySetSuboption {
diff --git a/jwt/options.yaml b/jwt/options.yaml
index e4761ec1..7380123d 100644
--- a/jwt/options.yaml
+++ b/jwt/options.yaml
@@ -47,6 +47,15 @@ options:
     comment: |
       WithAcceptableSkew specifies the duration in which exp and nbf
       claims may differ by. This value should be positive
+  - ident: Truncation
+    interface: ValidateOption
+    argument_type: time.Duration
+    comment: |
+      WithTruncation speficies the amount that should be used when
+      truncating time values used during time-based validation routines.
+      By default time values are truncated down to second accuracy.
+      If you want to use sub-second accuracy, you will need to set
+      this value to 0.
   - ident: Clock
     interface: ValidateOption
     argument_type: Clock
diff --git a/jwt/options_gen.go b/jwt/options_gen.go
index 98cccf36..dd40995c 100644
--- a/jwt/options_gen.go
+++ b/jwt/options_gen.go
@@ -137,6 +137,7 @@ type identNumericDateParsePrecision struct{}
 type identPedantic struct{}
 type identSignOption struct{}
 type identToken struct{}
+type identTruncation struct{}
 type identValidate struct{}
 type identValidator struct{}
 type identVerify struct{}
@@ -201,6 +202,10 @@ func (identToken) String() string {
 	return "WithToken"
 }
 
+func (identTruncation) String() string {
+	return "WithTruncation"
+}
+
 func (identValidate) String() string {
 	return "WithValidate"
 }
@@ -329,6 +334,15 @@ func WithToken(v Token) ParseOption {
 	return &parseOption{option.New(identToken{}, v)}
 }
 
+// WithTruncation speficies the amount that should be used when
+// truncating time values used during time-based validation routines.
+// By default time values are truncated down to second accuracy.
+// If you want to use sub-second accuracy, you will need to set
+// this value to 0.
+func WithTruncation(v time.Duration) ValidateOption {
+	return &validateOption{option.New(identTruncation{}, v)}
+}
+
 // WithValidate is passed to `Parse()` method to denote that the
 // validation of the JWT token should be performed (or not) after
 // a successful parsing of the incoming payload.
diff --git a/jwt/validate.go b/jwt/validate.go
index 315035e4..26e21d7e 100644
--- a/jwt/validate.go
+++ b/jwt/validate.go
@@ -3,6 +3,7 @@ package jwt
 import (
 	"context"
 	"fmt"
+	"log"
 	"strconv"
 	"time"
 )
@@ -44,6 +45,8 @@ func timeClaim(t Token, clock Clock, c string) time.Time {
 // that can control the behavior of this method.
 func Validate(t Token, options ...ValidateOption) error {
 	ctx := context.Background()
+	trunc := time.Second
+
 	var clock Clock = ClockFunc(time.Now)
 	var skew time.Duration
 	var validators = []Validator{
@@ -58,6 +61,8 @@ func Validate(t Token, options ...ValidateOption) error {
 			clock = o.Value().(Clock)
 		case identAcceptableSkew{}:
 			skew = o.Value().(time.Duration)
+		case identTruncation{}:
+			trunc = o.Value().(time.Duration)
 		case identContext{}:
 			ctx = o.Value().(context.Context)
 		case identValidator{}:
@@ -83,6 +88,7 @@ func Validate(t Token, options ...ValidateOption) error {
 
 	ctx = SetValidationCtxSkew(ctx, skew)
 	ctx = SetValidationCtxClock(ctx, clock)
+	ctx = SetValidationCtxTruncation(ctx, trunc)
 	for _, v := range validators {
 		if err := v.Validate(ctx, t); err != nil {
 			return err
@@ -124,8 +130,8 @@ func (iitr *isInTimeRange) Validate(ctx context.Context, t Token) ValidationErro
 	skew := ValidationCtxSkew(ctx)   // MUST be populated
 	// We don't check if the claims already exist, because we already did that
 	// by piggybacking on `required` check.
-	t1 := timeClaim(t, clock, iitr.c1).Truncate(time.Second)
-	t2 := timeClaim(t, clock, iitr.c2).Truncate(time.Second)
+	t1 := timeClaim(t, clock, iitr.c1)
+	t2 := timeClaim(t, clock, iitr.c2)
 	if iitr.less { // t1 - t2 <= iitr.dur
 		// t1 - t2 < iitr.dur + skew
 		if t1.Sub(t2) > iitr.dur+skew {
@@ -299,11 +305,20 @@ func (vf ValidatorFunc) Validate(ctx context.Context, tok Token) ValidationError
 
 type identValidationCtxClock struct{}
 type identValidationCtxSkew struct{}
+type identValidationCtxTruncation struct{}
 
 func SetValidationCtxClock(ctx context.Context, cl Clock) context.Context {
 	return context.WithValue(ctx, identValidationCtxClock{}, cl)
 }
 
+func SetValidationCtxTruncation(ctx context.Context, dur time.Duration) context.Context {
+	return context.WithValue(ctx, identValidationCtxTruncation{}, dur)
+}
+
+func SetValidationCtxSkew(ctx context.Context, dur time.Duration) context.Context {
+	return context.WithValue(ctx, identValidationCtxSkew{}, dur)
+}
+
 // ValidationCtxClock returns the Clock object associated with
 // the current validation context. This value will always be available
 // during validation of tokens.
@@ -312,15 +327,16 @@ func ValidationCtxClock(ctx context.Context) Clock {
 	return ctx.Value(identValidationCtxClock{}).(Clock)
 }
 
-func SetValidationCtxSkew(ctx context.Context, dur time.Duration) context.Context {
-	return context.WithValue(ctx, identValidationCtxSkew{}, dur)
-}
-
 func ValidationCtxSkew(ctx context.Context) time.Duration {
 	//nolint:forcetypeassert
 	return ctx.Value(identValidationCtxSkew{}).(time.Duration)
 }
 
+func ValidationCtxTruncation(ctx context.Context) time.Duration {
+	//nolint:forcetypeassert
+	return ctx.Value(identValidationCtxTruncation{}).(time.Duration)
+}
+
 // IsExpirationValid is one of the default validators that will be executed.
 // It does not need to be specified by users, but it exists as an
 // exported field so that you can check what it does.
@@ -333,14 +349,21 @@ func IsExpirationValid() Validator {
 }
 
 func isExpirationValid(ctx context.Context, t Token) ValidationError {
-	if tv := t.Expiration(); !tv.IsZero() && tv.Unix() != 0 {
-		clock := ValidationCtxClock(ctx) // MUST be populated
-		now := clock.Now().Truncate(time.Second)
-		ttv := tv.Truncate(time.Second)
-		skew := ValidationCtxSkew(ctx) // MUST be populated
-		if !now.Before(ttv.Add(skew)) {
-			return ErrTokenExpired()
-		}
+	tv := t.Expiration()
+	if tv.IsZero() || tv.Unix() == 0 {
+		return nil
+	}
+
+	clock := ValidationCtxClock(ctx)      // MUST be populated
+	skew := ValidationCtxSkew(ctx)        // MUST be populated
+	trunc := ValidationCtxTruncation(ctx) // MUST be populated
+
+	now := clock.Now().Truncate(trunc)
+	ttv := tv.Truncate(trunc)
+
+	// expiration date must be after NOW
+	if !now.Before(ttv.Add(skew)) {
+		return ErrTokenExpired()
 	}
 	return nil
 }
@@ -357,14 +380,21 @@ func IsIssuedAtValid() Validator {
 }
 
 func isIssuedAtValid(ctx context.Context, t Token) ValidationError {
-	if tv := t.IssuedAt(); !tv.IsZero() && tv.Unix() != 0 {
-		clock := ValidationCtxClock(ctx) // MUST be populated
-		now := clock.Now().Truncate(time.Second)
-		ttv := tv.Truncate(time.Second)
-		skew := ValidationCtxSkew(ctx) // MUST be populated
-		if now.Before(ttv.Add(-1 * skew)) {
-			return ErrInvalidIssuedAt()
-		}
+	tv := t.IssuedAt()
+	if tv.IsZero() || tv.Unix() == 0 {
+		return nil
+	}
+
+	clock := ValidationCtxClock(ctx)      // MUST be populated
+	skew := ValidationCtxSkew(ctx)        // MUST be populated
+	trunc := ValidationCtxTruncation(ctx) // MUST be populated
+
+	now := clock.Now().Truncate(trunc)
+	ttv := tv.Truncate(trunc)
+
+	log.Printf("now = %s, ttv = %s, skew = %s, trunc = %s", now.UTC(), ttv, skew, trunc)
+	if now.Before(ttv.Add(-1 * skew)) {
+		return ErrInvalidIssuedAt()
 	}
 	return nil
 }
@@ -381,15 +411,24 @@ func IsNbfValid() Validator {
 }
 
 func isNbfValid(ctx context.Context, t Token) ValidationError {
-	if tv := t.NotBefore(); !tv.IsZero() && tv.Unix() != 0 {
-		clock := ValidationCtxClock(ctx) // MUST be populated
-		now := clock.Now().Truncate(time.Second)
-		ttv := tv.Truncate(time.Second)
-		skew := ValidationCtxSkew(ctx) // MUST be populated
-		// now cannot be before t, so we check for now > t - skew
-		if !now.Equal(ttv) && !now.After(ttv.Add(-1*skew)) {
-			return ErrTokenNotYetValid()
-		}
+	tv := t.NotBefore()
+	if tv.IsZero() || tv.Unix() == 0 {
+		return nil
+	}
+
+	clock := ValidationCtxClock(ctx)      // MUST be populated
+	skew := ValidationCtxSkew(ctx)        // MUST be populated
+	trunc := ValidationCtxTruncation(ctx) // MUST be populated
+
+	// Truncation always happens even for trunc = 0 because
+	// we also use this to strip monotonic clocks
+	now := clock.Now().Truncate(trunc)
+	ttv := tv.Truncate(trunc)
+
+	// "now" cannot be before t - skew, so we check for now > t - skew
+	ttv = ttv.Add(-1 * skew)
+	if now.Before(ttv) {
+		return ErrTokenNotYetValid()
 	}
 	return nil
 }
diff --git a/tools/cmd/genjwk/main.go b/tools/cmd/genjwk/main.go
index d0d4b77a..16fbb907 100644
--- a/tools/cmd/genjwk/main.go
+++ b/tools/cmd/genjwk/main.go
@@ -669,7 +669,7 @@ func generateGenericHeaders(fields codegen.FieldList) error {
 	o.L("// All fields are copied onto the new public key, except for those that are not allowed.")
 	o.L("//\n// If the key is already a public key, it returns a new copy minus the disallowed fields as above.")
 	o.L("PublicKey() (Key, error)")
-	o.LL("// KeyType returns the `kid` of a JWK")
+	o.LL("// KeyType returns the `kty` of a JWK")
 	o.L("KeyType() jwa.KeyType")
 	for _, f := range fields {
 		o.L("// %s returns `%s` of a JWK", f.GetterMethod(true), f.JSON())
diff --git a/tools/cmd/genoptions/main.go b/tools/cmd/genoptions/main.go
index aab1d0f8..d3296961 100644
--- a/tools/cmd/genoptions/main.go
+++ b/tools/cmd/genoptions/main.go
@@ -121,6 +121,7 @@ func genOptions(objects *Objects) error {
 	o.LL(`package %s`, objects.PackageName)
 
 	imports := append(objects.Imports, []string{
+		`io/fs`, // for some reason without this the goimports in my environment tries to import a differnet package
 		`github.com/lestrrat-go/jwx/v2/jwa`,
 		`github.com/lestrrat-go/jwx/v2/jwe`,
 		`github.com/lestrrat-go/jwx/v2/jwk`,


Test patch:
diff --git a/cert/chain_test.go b/cert/chain_test.go
index 2be34529..03dbce08 100644
--- a/cert/chain_test.go
+++ b/cert/chain_test.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/lestrrat-go/jwx/v2/cert"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 var certBytes = []byte(`MIICdDCCAd2gAwIBAgIUEpq1vvAyaiEKhgEE/UKykUcnXi4wDQYJKoZIhvcNAQEL
@@ -71,4 +72,9 @@ func TestChain(t *testing.T) {
 			return
 		}
 	}
+
+	for _, i := range []int{-1, chain.Len()} {
+		_, ok := chain.Get(i)
+		require.False(t, ok, `out of bounds should properly error`)
+	}
 }
diff --git a/jws/jws_test.go b/jws/jws_test.go
index 5ac46c78..648b2012 100644
--- a/jws/jws_test.go
+++ b/jws/jws_test.go
@@ -30,6 +30,7 @@ import (
 	"github.com/lestrrat-go/jwx/v2/jws"
 	"github.com/lestrrat-go/jwx/v2/x25519"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 const examplePayload = `{"iss":"joe",` + "\r\n" + ` "exp":1300819380,` + "\r\n" + ` "http://example.com/is_root":true}`
@@ -1065,6 +1066,92 @@ func TestReadFile(t *testing.T) {
 	}
 }
 
+func TestVerifyNonUniqueKid(t *testing.T) {
+	t.Parallel()
+	const payload = "Lorem ipsum"
+	const kid = "notUniqueKid"
+	privateKey, err := jwxtest.GenerateRsaJwk()
+	if !assert.NoError(t, err, "jwxtest.GenerateJwk should succeed") {
+		return
+	}
+	_ = privateKey.Set(jwk.KeyIDKey, kid)
+	signed, err := jws.Sign([]byte(payload), jws.WithKey(jwa.RS256, privateKey))
+	if !assert.NoError(t, err, `jws.Sign should succeed`) {
+		return
+	}
+	correctKey, _ := jwk.PublicKeyOf(privateKey)
+	_ = correctKey.Set(jwk.AlgorithmKey, jwa.RS256)
+
+	makeSet := func(keys ...jwk.Key) jwk.Set {
+		set := jwk.NewSet()
+		for _, key := range keys {
+			_ = set.AddKey(key)
+		}
+		return set
+	}
+
+	testcases := []struct {
+		Name string
+		Key  func() jwk.Key // Generates the "wrong" key
+	}{
+		{
+			Name: `match 2 keys via same "kid"`,
+			Key: func() jwk.Key {
+				privateKey, _ := jwxtest.GenerateRsaJwk()
+				wrongKey, _ := jwk.PublicKeyOf(privateKey)
+				_ = wrongKey.Set(jwk.KeyIDKey, kid)
+				_ = wrongKey.Set(jwk.AlgorithmKey, jwa.RS256)
+				return wrongKey
+			},
+		},
+		{
+			Name: `match 2 keys via same "kid", same key value but different alg`,
+			Key: func() jwk.Key {
+				wrongKey, _ := correctKey.Clone()
+				_ = wrongKey.Set(jwk.KeyIDKey, kid)
+				_ = wrongKey.Set(jwk.AlgorithmKey, jwa.RS512)
+				return wrongKey
+			},
+		},
+		{
+			Name: `match 2 keys via same "kid", same key type but different alg`,
+			Key: func() jwk.Key {
+				privateKey, _ := jwxtest.GenerateRsaJwk()
+				wrongKey, _ := jwk.PublicKeyOf(privateKey)
+				_ = wrongKey.Set(jwk.KeyIDKey, kid)
+				_ = wrongKey.Set(jwk.AlgorithmKey, jwa.RS512)
+				return wrongKey
+			},
+		},
+		{
+			Name: `match 2 keys via same "kid" and different key type / alg`,
+			Key: func() jwk.Key {
+				privateKey, _ := jwxtest.GenerateEcdsaKey(jwa.P256)
+				wrongKey, _ := jwk.PublicKeyOf(privateKey)
+				_ = wrongKey.Set(jwk.KeyIDKey, kid)
+				_ = wrongKey.Set(jwk.AlgorithmKey, jwa.ES256K)
+				return wrongKey
+			},
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.Name, func(t *testing.T) {
+			t.Parallel()
+			wrongKey := tc.Key()
+			// Try matching in different orders
+			for _, set := range []jwk.Set{makeSet(wrongKey, correctKey), makeSet(correctKey, wrongKey)} {
+				var usedKey jwk.Key
+				_, err = jws.Verify(signed, jws.WithKeySet(set, jws.WithMultipleKeysPerKeyID(true)), jws.WithKeyUsed(&usedKey))
+				if !assert.NoError(t, err, `jws.Verify should succeed`) {
+					return
+				}
+				require.Equal(t, usedKey, correctKey)
+			}
+		})
+	}
+}
+
 func TestVerifySet(t *testing.T) {
 	t.Parallel()
 	const payload = "Lorem ipsum"
diff --git a/jws/options_gen_test.go b/jws/options_gen_test.go
index 15b69228..fd0164a4 100644
--- a/jws/options_gen_test.go
+++ b/jws/options_gen_test.go
@@ -18,6 +18,7 @@ func TestOptionIdent(t *testing.T) {
 	require.Equal(t, "WithKeyProvider", identKeyProvider{}.String())
 	require.Equal(t, "WithKeyUsed", identKeyUsed{}.String())
 	require.Equal(t, "WithMessage", identMessage{}.String())
+	require.Equal(t, "WithMultipleKeysPerKeyID", identMultipleKeysPerKeyID{}.String())
 	require.Equal(t, "WithPretty", identPretty{}.String())
 	require.Equal(t, "WithProtectedHeaders", identProtectedHeaders{}.String())
 	require.Equal(t, "WithPublicHeaders", identPublicHeaders{}.String())
diff --git a/jwt/options_gen_test.go b/jwt/options_gen_test.go
index 060436af..90fcd3cc 100644
--- a/jwt/options_gen_test.go
+++ b/jwt/options_gen_test.go
@@ -24,6 +24,7 @@ func TestOptionIdent(t *testing.T) {
 	require.Equal(t, "WithPedantic", identPedantic{}.String())
 	require.Equal(t, "WithSignOption", identSignOption{}.String())
 	require.Equal(t, "WithToken", identToken{}.String())
+	require.Equal(t, "WithTruncation", identTruncation{}.String())
 	require.Equal(t, "WithValidate", identValidate{}.String())
 	require.Equal(t, "WithValidator", identValidator{}.String())
 	require.Equal(t, "WithVerify", identVerify{}.String())
diff --git a/jwt/validate_test.go b/jwt/validate_test.go
index d4241407..40d2893c 100644
--- a/jwt/validate_test.go
+++ b/jwt/validate_test.go
@@ -3,12 +3,14 @@ package jwt_test
 import (
 	"context"
 	"errors"
+	"log"
 	"testing"
 	"time"
 
 	"github.com/lestrrat-go/jwx/v2/internal/json"
 	"github.com/lestrrat-go/jwx/v2/jwt"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestGHIssue10(t *testing.T) {
@@ -102,27 +104,70 @@ func TestGHIssue10(t *testing.T) {
 	})
 	t.Run(jwt.IssuedAtKey, func(t *testing.T) {
 		t.Parallel()
-		t1 := jwt.New()
-		t1.Set(jwt.IssuedAtKey, time.Now().Add(365*24*time.Second))
+		tm := time.Now()
+		t1, err := jwt.NewBuilder().
+			Claim(jwt.IssuedAtKey, tm).
+			Build()
+		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
+			return
+		}
 
-		t.Run(`iat too far in the past`, func(t *testing.T) {
-			err := jwt.Validate(t1)
-			if !assert.Error(t, err, `jwt.Validate should fail`) {
-				return
-			}
+		testcases := []struct {
+			Name    string
+			Options []jwt.ValidateOption
+			Error   bool
+		}{
+			{
+				Name:  `clock is set to before iat`,
+				Error: true,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Hour) })),
+				},
+			},
+			{
+				// This works because the sub-second difference is rounded
+				Name: `clock is set to some sub-seconds before iat`,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Millisecond) })),
+				},
+			},
+			{
+				Name:  `clock is set to some sub-seconds before iat (trunc = 0)`,
+				Error: true,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Millisecond) })),
+					jwt.WithTruncation(0),
+				},
+			},
+		}
 
-			if !assert.True(t, errors.Is(err, jwt.ErrInvalidIssuedAt()), `error should be jwt.ErrInvalidIssuedAt`) {
-				return
-			}
+		for _, tc := range testcases {
+			tc := tc
+			t.Run(tc.Name, func(t *testing.T) {
+				log.Printf("%s", tc.Name)
+				err := jwt.Validate(t1, tc.Options...)
+				if !tc.Error {
+					assert.NoError(t, err, `jwt.Validate should succeed`)
+					return
+				}
 
-			if !assert.False(t, errors.Is(err, jwt.ErrTokenNotYetValid()), `error should be not ErrNotYetValid`) {
-				return
-			}
+				if !assert.Error(t, err, `jwt.Validate should fail`) {
+					return
+				}
 
-			if !assert.True(t, jwt.IsValidationError(err), `error should be a validation error`) {
-				return
-			}
-		})
+				if !assert.True(t, errors.Is(err, jwt.ErrInvalidIssuedAt()), `error should be jwt.ErrInvalidIssuedAt`) {
+					return
+				}
+
+				if !assert.False(t, errors.Is(err, jwt.ErrTokenNotYetValid()), `error should be not ErrNotYetValid`) {
+					return
+				}
+
+				if !assert.True(t, jwt.IsValidationError(err), `error should be a validation error`) {
+					return
+				}
+			})
+		}
 	})
 	t.Run(jwt.AudienceKey, func(t *testing.T) {
 		t.Parallel()
@@ -219,40 +264,61 @@ func TestGHIssue10(t *testing.T) {
 			},
 			{ // This should succeed, because we have given a time
 				// that is well enough into the future
-				Name: `clock is set to after time in nbf`,
+				Name: `clock is set to some time after in nbf`,
 				Options: []jwt.ValidateOption{
 					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(time.Hour) })),
 				},
 			},
 			{ // This should succeed, the time == NotBefore time
-				// Note, this may fail if you are return a monotonic clock
+				// Note, this could fail if you are returning a monotonic clock
+				// and we didn't do something about it
 				Name: `clock is set to the same time as nbf`,
 				Options: []jwt.ValidateOption{
 					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm })),
 				},
 			},
+			{
+				Name:  `clock is set to some subseconds before nbf`,
+				Error: true,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Millisecond) })),
+					jwt.WithTruncation(0),
+				},
+			},
+			{
+				Name: `clock is set to some subseconds before nbf (but truncation = default)`,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Millisecond) })),
+				},
+			},
+			{
+				Name: `clock is set to some subseconds after nbf`,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(time.Millisecond) })),
+					jwt.WithTruncation(0),
+				},
+			},
 		}
 		for _, tc := range testcases {
 			tc := tc
 			t.Run(tc.Name, func(t *testing.T) {
-				if tc.Error {
-					err := jwt.Validate(t1, tc.Options...)
-					if !assert.Error(t, err, "token.Validate should fail") {
-						return
-					}
-					if !assert.True(t, errors.Is(err, jwt.ErrTokenNotYetValid()), `error should be ErrTokenNotYetValid`) {
-						return
-					}
-					if !assert.False(t, errors.Is(err, jwt.ErrTokenExpired()), `error should not be ErrTokenExpierd`) {
-						return
-					}
-					if !assert.True(t, jwt.IsValidationError(err), `error should be a validation error`) {
-						return
-					}
-				} else {
-					if !assert.NoError(t, jwt.Validate(t1, tc.Options...), "token.Validate should succeed") {
-						return
-					}
+				err := jwt.Validate(t1, tc.Options...)
+				if !tc.Error {
+					assert.NoError(t, err, "token.Validate should succeed")
+					return
+				}
+
+				if !assert.Error(t, err, "token.Validate should fail") {
+					return
+				}
+				if !assert.True(t, errors.Is(err, jwt.ErrTokenNotYetValid()), `error should be ErrTokenNotYetValid`) {
+					return
+				}
+				if !assert.False(t, errors.Is(err, jwt.ErrTokenExpired()), `error should not be ErrTokenExpierd`) {
+					return
+				}
+				if !assert.True(t, jwt.IsValidationError(err), `error should be a validation error`) {
+					return
 				}
 			})
 		}
@@ -265,49 +331,81 @@ func TestGHIssue10(t *testing.T) {
 			// issuedat = 1 Hr before current time
 			Claim(jwt.IssuedAtKey, tm.Add(-1*time.Hour)).
 			// valid for 2 minutes only from IssuedAt
-			Claim(jwt.ExpirationKey, tm.Add(-58*time.Minute)).
+			Claim(jwt.ExpirationKey, tm).
 			Build()
 		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
 			return
 		}
 
-		// This should fail, because exp is set in the past
-		t.Run("exp set in the past", func(t *testing.T) {
-			t.Parallel()
-			err := jwt.Validate(t1)
-			if !assert.Error(t, err, "token.Validate should fail") {
-				return
-			}
-			if !assert.True(t, errors.Is(err, jwt.ErrTokenExpired()), `error should be ErrTokenExpired`) {
-				return
-			}
-			if !assert.False(t, errors.Is(err, jwt.ErrTokenNotYetValid()), `error should be not ErrNotYetValid`) {
-				return
-			}
-			if !assert.True(t, jwt.IsValidationError(err), `error should be a validation error`) {
-				return
-			}
-		})
-		// This should succeed, because we have given big skew
-		// that is well enough to get us accepted
-		t.Run("exp is set in the past, but acceptable skew is large", func(t *testing.T) {
-			t.Parallel()
-			if !assert.NoError(t, jwt.Validate(t1, jwt.WithAcceptableSkew(time.Hour)), "token.Validate should succeed (1)") {
-				return
-			}
-		})
+		testcases := []struct {
+			Name    string
+			Options []jwt.ValidateOption
+			Error   bool
+		}{
+			{
+				Name:  `clock is not modified (exp < now)`,
+				Error: true,
+			},
+			{
+				Name: `clock is set to some time before exp`,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Hour) })),
+				},
+			},
+			{ // This should fail, the time == Expiration.
+				// Note, this could fail if you are returning a monotonic clock
+				// and we didn't do something about it
+				Name:  `clock is set to same time as exp`,
+				Error: true,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm })),
+				},
+			},
+			{
+				Name:  `clock is set to some subseconds after exp`,
+				Error: true,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(time.Millisecond) })),
+					jwt.WithTruncation(0),
+				},
+			},
+			{
+				Name:  `clock is set to some subseconds after exp (but truncation = default)`,
+				Error: true,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(time.Millisecond) })),
+				},
+			},
+			{
+				Name: `clock is set to some subseconds before exp`,
+				Options: []jwt.ValidateOption{
+					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Millisecond) })),
+					jwt.WithTruncation(0),
+				},
+			},
+		}
 
-		// This should succeed, because we have given a time
-		// that is well enough into the past
-		t.Run("exp is set in the past, but clock is also set in the past", func(t *testing.T) {
-			t.Parallel()
-			clock := jwt.ClockFunc(func() time.Time {
-				return tm.Add(-59 * time.Minute)
+		for _, tc := range testcases {
+			tc := tc
+			t.Run(tc.Name, func(t *testing.T) {
+				err := jwt.Validate(t1, tc.Options...)
+				if !tc.Error {
+					assert.NoError(t, err, `jwt.Validate should succeed`)
+					return
+				}
+
+				require.Error(t, err, `jwt.Validate should fail`)
+				if !assert.False(t, errors.Is(err, jwt.ErrTokenNotYetValid()), `error should not be ErrTokenNotYetValid`) {
+					return
+				}
+				if !assert.True(t, errors.Is(err, jwt.ErrTokenExpired()), `error should be ErrTokenExpierd`) {
+					return
+				}
+				if !assert.True(t, jwt.IsValidationError(err), `error should be a validation error`) {
+					return
+				}
 			})
-			if !assert.NoError(t, jwt.Validate(t1, jwt.WithClock(clock)), "token.Validate should succeed (2)") {
-				return
-			}
-		})
+		}
 	})
 	t.Run("Unix zero times", func(t *testing.T) {
 		t.Parallel()