Repository: google/exposure-notifications-server
Pull request #1519 (2021-04-29 01:39:58)

add metrics for future dated JWT
## Proposed Changes

* introduce a new metric for future JWT (NBF and/or IAT in the future)

**Release Note**


```release-note
Adds a new metric 'en-server/jwt_not_yet_valid' that will be incremented when the server processes a JWT that is future dated according to either the IAT or NBF field of a properly signed verification certificate JWT.
```


Patch:
diff --git a/internal/publish/metrics.go b/internal/publish/metrics.go
index 53b20b3..78b7622 100644
--- a/internal/publish/metrics.go
+++ b/internal/publish/metrics.go
@@ -29,6 +29,9 @@ var (
 
 	mLatencyMs = stats.Float64(publishMetricsPrefix+"requests", "publish requests", stats.UnitMilliseconds)
 
+	mJWTNotYetValid = stats.Int64(publishMetricsPrefix+"jwt_not_yet_valid",
+		"a certificate was presented with an IAT or NBF time that is in the past according to thie server", stats.UnitDimensionless)
+
 	mNoPublicKey = stats.Int64(publishMetricsPrefix+"no_public_keys",
 		"uploads where there is no public key", stats.UnitDimensionless)
 
@@ -100,6 +103,13 @@ func init() {
 			Measure:     mVerificationBypassed,
 			Aggregation: view.Sum(),
 		},
+		{
+			Name:        metrics.MetricRoot + "jwt_not_yet_valid",
+			Description: "Total count of instances where a verification certificate is in the future",
+			Measure:     mJWTNotYetValid,
+			Aggregation: view.Sum(),
+			TagKeys:     missingPublicKeyTags,
+		},
 		// v1 and v1alpha1
 		{
 			Name:        metrics.MetricRoot + "padding_failed",
diff --git a/internal/publish/publish.go b/internal/publish/publish.go
index d0c53de..7bd342b 100644
--- a/internal/publish/publish.go
+++ b/internal/publish/publish.go
@@ -327,6 +327,14 @@ func (s *Server) process(ctx context.Context, data *verifyapi.Publish, platform
 				}
 			}
 
+			if errors.Is(err, verification.ErrNotValidYet) {
+				logger.Warnw("received future dated verification certificate", "healthAuthorityID", data.HealthAuthorityID)
+				tags := []tag.Mutator{tag.Upsert(healthAuthorityIDTag, data.HealthAuthorityID)}
+				if err := stats.RecordWithTags(ctx, tags, mJWTNotYetValid.M(1)); err != nil {
+					logger.Errorw("failed to record stats for missing public key", "error", err, "healthAuthorityID", data.HealthAuthorityID)
+				}
+			}
+
 			message := fmt.Sprintf("unable to validate diagnosis verification: %v", err)
 			if s.config.DebugLogBadCertificates {
 				logger.Errorw(message, "error", err, "jwt", data.VerificationPayload)
diff --git a/internal/verification/phaverify.go b/internal/verification/phaverify.go
index 7aa300d..652d50e 100644
--- a/internal/verification/phaverify.go
+++ b/internal/verification/phaverify.go
@@ -35,8 +35,11 @@ import (
 	"github.com/dgrijalva/jwt-go"
 )
 
-// ErrNoPublicKeys indicates no public keys were found when verifying the certificate.
-var ErrNoPublicKeys = errors.New("no active public keys for health authority")
+var (
+	// ErrNoPublicKeys indicates no public keys were found when verifying the certificate.
+	ErrNoPublicKeys = errors.New("no active public keys for health authority")
+	ErrNotValidYet  = errors.New("not valid yet (NBF or IAT) in the future")
+)
 
 // Verifier can be used to verify public health authority diagnosis verification certificates.
 type Verifier struct {
@@ -72,6 +75,7 @@ func (v *Verifier) VerifyDiagnosisCertificate(ctx context.Context, authApp *aamo
 	var claims *verifyapi.VerificationClaims
 
 	// Unpack JWT so we can determine issuer and key version.
+	// ParseWithClaims also calls .Valid() on the parsed token.
 	token, err := jwt.ParseWithClaims(publish.VerificationPayload, &verifyapi.VerificationClaims{}, func(token *jwt.Token) (interface{}, error) {
 		if method, ok := token.Method.(*jwt.SigningMethodECDSA); !ok || method.Name != jwt.SigningMethodES256.Name {
 			return nil, fmt.Errorf("unsupported signing method, must be %v", jwt.SigningMethodES256.Name)
@@ -129,15 +133,20 @@ func (v *Verifier) VerifyDiagnosisCertificate(ctx context.Context, authApp *aamo
 		return nil, ErrNoPublicKeys
 	})
 	if err != nil {
+		// Check for specific errors in the bitmask that may exist and
+		// convert them to application local errors.
+		validationError := new(jwt.ValidationError)
+		if errors.As(err, &validationError) {
+			if mask := validationError.Errors; mask&jwt.ValidationErrorIssuedAt != 0 || mask&jwt.ValidationErrorNotValidYet != 0 {
+				return nil, ErrNotValidYet
+			}
+		}
 		return nil, err
 	}
 
 	if !token.Valid {
 		return nil, fmt.Errorf("invalid verificationPayload")
 	}
-	if err := claims.Valid(); err != nil {
-		return nil, err
-	}
 
 	// JWT is valid and signature is valid.
 	// This is chacked after the signature verification to prevent timing attacks.


Test patch:
diff --git a/internal/publish/publish_test.go b/internal/publish/publish_test.go
index 0dea439..88ff302 100644
--- a/internal/publish/publish_test.go
+++ b/internal/publish/publish_test.go
@@ -426,7 +426,7 @@ func TestPublishWithBypass(t *testing.T) {
 			Regions:   []string{regions.current()},
 			JWTTiming: time.Hour,
 			Code:      http.StatusUnauthorized,
-			Error:     "unable to validate diagnosis verification: Token used before issued",
+			Error:     "unable to validate diagnosis verification: not valid yet (NBF or IAT) in the future",
 		},
 		{
 			Name: "certificate expired",
diff --git a/internal/verification/phaverify_test.go b/internal/verification/phaverify_test.go
index 00e3f18..2ea224d 100644
--- a/internal/verification/phaverify_test.go
+++ b/internal/verification/phaverify_test.go
@@ -75,7 +75,7 @@ func TestVerifyCertificate(t *testing.T) {
 		{
 			Name:  "future",
 			Warp:  1 * time.Hour,
-			Error: "Token used before issued",
+			Error: ErrNotValidYet.Error(),
 		},
 		{
 			Name:          "invalid hmac",