Bug 980478 - Generate assertions with no iat and exp: 9999999999999L. r=rnewman
authorNick Alexander <nalexander@mozilla.com>
Mon, 10 Mar 2014 21:04:25 -0700
changeset 191206 923bd540eddefc95093a105630b44c44529bcb05
parent 191205 ef0be52057a8a1b220d5e0dd5cfbe9d18ee06c2b
child 191207 922daaaeb05ac2efafc920e0984cd9ad4363595d
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs980478
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 980478 - Generate assertions with no iat and exp: 9999999999999L. r=rnewman
mobile/android/base/browserid/JSONWebTokenUtils.java
mobile/android/base/browserid/MockMyIDTokenFactory.java
mobile/android/base/fxa/FxAccountConstants.java.in
mobile/android/base/fxa/login/Married.java
mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
--- a/mobile/android/base/browserid/JSONWebTokenUtils.java
+++ b/mobile/android/base/browserid/JSONWebTokenUtils.java
@@ -4,17 +4,19 @@
 
 package org.mozilla.gecko.browserid;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Map;
+import java.util.TreeMap;
 
+import org.json.simple.JSONObject;
 import org.json.simple.parser.ParseException;
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.apache.commons.codec.binary.StringUtils;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.Utils;
 
 /**
@@ -23,16 +25,17 @@ import org.mozilla.gecko.sync.Utils;
  * Reverse-engineered from the Node.js jwcrypto library at
  * <a href="https://github.com/mozilla/jwcrypto">https://github.com/mozilla/jwcrypto</a>
  * and informed by the informal draft standard "JSON Web Token (JWT)" at
  * <a href="http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html">http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html</a>.
  */
 public class JSONWebTokenUtils {
   public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
   public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
+  public static final long DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS = 9999999999999L;
   public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1";
   public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1";
 
   public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException  {
     return encode(payload, privateKey, null);
   }
 
   protected static String encode(String payload, SigningPrivateKey privateKey, Map<String, Object> headerFields) throws UnsupportedEncodingException, GeneralSecurityException  {
@@ -65,73 +68,89 @@ public class JSONWebTokenUtils {
     boolean verifies = publicKey.verifyMessage(message, signature);
     if (!verifies) {
       throw new GeneralSecurityException("bad signature");
     }
     String payload = StringUtils.newStringUtf8(Base64.decodeBase64(segments[1]));
     return payload;
   }
 
-  protected static String getPayloadString(String payloadString, String issuer,
-      long issuedAt, String audience, long expiresAt) throws NonObjectJSONException,
+  /**
+   * Public for testing.
+   */
+  @SuppressWarnings("unchecked")
+  public static String getPayloadString(String payloadString, String audience, String issuer,
+      Long issuedAt, long expiresAt) throws NonObjectJSONException,
       IOException, ParseException {
     ExtendedJSONObject payload;
     if (payloadString != null) {
       payload = new ExtendedJSONObject(payloadString);
     } else {
       payload = new ExtendedJSONObject();
     }
-    payload.put("iss", issuer);
-    payload.put("iat", issuedAt);
     if (audience != null) {
       payload.put("aud", audience);
     }
+    payload.put("iss", issuer);
+    if (issuedAt != null) {
+      payload.put("iat", issuedAt);
+    }
     payload.put("exp", expiresAt);
-    return payload.toJSONString();
+    // TreeMap so that keys are sorted. A small attempt to keep output stable over time.
+    return JSONObject.toJSONString(new TreeMap<Object, Object>(payload.object));
   }
 
   protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException, ParseException  {
     ExtendedJSONObject payload = new ExtendedJSONObject();
     ExtendedJSONObject principal = new ExtendedJSONObject();
     principal.put("email", email);
     payload.put("principal", principal);
     payload.put("public-key", publicKeyToSign.toJSONObject());
     return payload.toJSONString();
   }
 
   public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email,
       String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
     String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email);
-    String payloadString = getPayloadString(certificatePayloadString, issuer, issuedAt, null, expiresAt);
+    String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt);
     return JSONWebTokenUtils.encode(payloadString, privateKey);
   }
 
-  public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
-    String issuer = DEFAULT_CERTIFICATE_ISSUER;
-    long issuedAt = System.currentTimeMillis();
-    long durationInMilliseconds = DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
-    return createCertificate(publicKeyToSign, email, issuer, issuedAt, issuedAt + durationInMilliseconds, privateKey);
-  }
-
+  /**
+   * Create a Browser ID assertion.
+   *
+   * @param privateKeyToSignWith
+   *          private key to sign assertion with.
+   * @param certificate
+   *          to include in assertion; no attempt is made to ensure the
+   *          certificate is valid, or corresponds to the private key, or any
+   *          other condition.
+   * @param audience
+   *          to produce assertion for.
+   * @param issuer
+   *          to produce assertion for.
+   * @param issuedAt
+   *          timestamp for assertion, in milliseconds since the epoch; if null,
+   *          no timestamp is included.
+   * @param expiresAt
+   *          expiration timestamp for assertion, in milliseconds since the epoch.
+   * @return assertion.
+   * @throws NonObjectJSONException
+   * @throws IOException
+   * @throws ParseException
+   * @throws GeneralSecurityException
+   */
   public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience,
-      String issuer, long issuedAt, long durationInMilliseconds) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
-    long expiresAt = issuedAt + durationInMilliseconds;
+      String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
     String emptyAssertionPayloadString = "{}";
-    String payloadString = getPayloadString(emptyAssertionPayloadString, issuer, issuedAt, audience, expiresAt);
+    String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt);
     String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith);
     return certificate + "~" + signature;
   }
 
-  public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
-    String issuer = DEFAULT_ASSERTION_ISSUER;
-    long issuedAt = System.currentTimeMillis();
-    long durationInMilliseconds = DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS;
-    return createAssertion(privateKeyToSignWith, certificate, audience, issuer, issuedAt, durationInMilliseconds);
-  }
-
   /**
    * For debugging only!
    *
    * @param input
    *          certificate to dump.
    * @return non-null object with keys header, payload, signature if the
    *         certificate is well-formed.
    */
--- a/mobile/android/base/browserid/MockMyIDTokenFactory.java
+++ b/mobile/android/base/browserid/MockMyIDTokenFactory.java
@@ -35,28 +35,27 @@ public class MockMyIDTokenFactory {
    * mockmyid.com's private key.
    *
    * @param publicKeyToSign
    *          public key to sign.
    * @param username
    *          sign username@mockmyid.com
    * @param issuedAt
    *          timestamp for certificate, in milliseconds since the epoch.
-   * @param durationInMilliseconds
-   *          lifespan of certificate, in milliseconds.
+   * @param expiresAt
+   *          expiration timestamp for certificate, in milliseconds since the epoch.
    * @return encoded certificate string.
    * @throws Exception
    */
   public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, String username,
-      final long issuedAt, final long durationInMilliseconds)
+      final long issuedAt, final long expiresAt)
           throws Exception {
     if (!username.endsWith("@mockmyid.com")) {
       username = username + "@mockmyid.com";
     }
-    long expiresAt = issuedAt + durationInMilliseconds;
     SigningPrivateKey mockMyIdPrivateKey = getMockMyIDPrivateKey();
     return JSONWebTokenUtils.createCertificate(publicKeyToSign, username, "mockmyid.com", issuedAt, expiresAt, mockMyIdPrivateKey);
   }
 
   /**
    * Sign a public key asserting ownership of username@mockmyid.com with
    * mockmyid.com's private key.
    *
@@ -64,62 +63,66 @@ public class MockMyIDTokenFactory {
    *          public key to sign.
    * @param username
    *          sign username@mockmyid.com
    * @return encoded certificate string.
    * @throws Exception
    */
   public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, final String username)
       throws Exception {
-    return createMockMyIDCertificate(publicKeyToSign, username,
-        System.currentTimeMillis(), JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS );
+    long ciat = System.currentTimeMillis();
+    long cexp = ciat + JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
+    return createMockMyIDCertificate(publicKeyToSign, username, ciat, cexp);
   }
 
   /**
    * Generate an assertion asserting ownership of username@mockmyid.com to a
    * relying party. The underlying certificate is signed by mockymid.com's
    * private key.
    *
    * @param keyPair
    *          to sign with.
    * @param username
    *          sign username@mockmyid.com.
    * @param certificateIssuedAt
    *          timestamp for certificate, in milliseconds since the epoch.
-   * @param certificateDurationInMilliseconds
-   *          lifespan of certificate, in milliseconds.
+   * @param certificateExpiresAt
+   *          expiration timestamp for certificate, in milliseconds since the epoch.
    * @param assertionIssuedAt
-   *          timestamp for assertion, in milliseconds since the epoch.
-   * @param assertionDurationInMilliseconds
-   *          lifespan of assertion, in milliseconds.
+   *          timestamp for assertion, in milliseconds since the epoch; if null,
+   *          no timestamp is included.
+   * @param assertionExpiresAt
+   *          expiration timestamp for assertion, in milliseconds since the epoch.
    * @return encoded assertion string.
    * @throws Exception
    */
   public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience,
-      long certificateIssuedAt, long certificateDurationInMilliseconds,
-      long assertionIssuedAt, long assertionDurationInMilliseconds)
+      long certificateIssuedAt, long certificateExpiresAt,
+      Long assertionIssuedAt, long assertionExpiresAt)
           throws Exception {
     String certificate = createMockMyIDCertificate(keyPair.getPublic(), username,
-        certificateIssuedAt, certificateDurationInMilliseconds);
+        certificateIssuedAt, certificateExpiresAt);
     return JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience,
-        JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, assertionIssuedAt, assertionDurationInMilliseconds);
+        JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, assertionIssuedAt, assertionExpiresAt);
   }
 
   /**
    * Generate an assertion asserting ownership of username@mockmyid.com to a
    * relying party. The underlying certificate is signed by mockymid.com's
    * private key.
    *
    * @param keyPair
    *          to sign with.
    * @param username
    *          sign username@mockmyid.com.
    * @return encoded assertion string.
    * @throws Exception
    */
   public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience)
       throws Exception {
-    long now = System.currentTimeMillis();
+    long ciat = System.currentTimeMillis();
+    long cexp = ciat + JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
+    long aiat = ciat + 1;
+    long aexp = aiat + JSONWebTokenUtils.DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS;
     return createMockMyIDAssertion(keyPair, username, audience,
-        now, JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS,
-        now + 1, JSONWebTokenUtils.DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS);
+        ciat, cexp, aiat, aexp);
   }
 }
--- a/mobile/android/base/fxa/FxAccountConstants.java.in
+++ b/mobile/android/base/fxa/FxAccountConstants.java.in
@@ -9,16 +9,18 @@ import org.mozilla.gecko.background.comm
 
 public class FxAccountConstants {
   public static final String GLOBAL_LOG_TAG = "FxAccounts";
   public static final String ACCOUNT_TYPE = "@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@";
 
   public static final String DEFAULT_AUTH_SERVER_ENDPOINT = "https://api.accounts.firefox.com/v1";
   public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.services.mozilla.com/1.0/sync/1.5";
 
+  public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://token.stage.mozaws.net/1.0/sync/1.5";
+
   // For extra debugging.  Not final so it can be changed from Fennec, or from
   // an add-on.
   public static boolean LOG_PERSONAL_INFORMATION = false;
 
   public static void pii(String tag, String message) {
     if (LOG_PERSONAL_INFORMATION) {
       Logger.info(tag, "$$FxA PII$$: " + message);
     }
--- a/mobile/android/base/fxa/login/Married.java
+++ b/mobile/android/base/fxa/login/Married.java
@@ -51,18 +51,21 @@ public class Married extends TokensAndKe
     return o;
   }
 
   @Override
   public void execute(final ExecuteDelegate delegate) {
     delegate.handleTransition(new LogMessage("staying married"), this);
   }
 
-  public String generateAssertion(String audience, String issuer, long issuedAt, long durationInMilliseconds) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException {
-    String assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience, issuer, issuedAt, durationInMilliseconds);
+  public String generateAssertion(String audience, String issuer) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException {
+    // We generate assertions with no iat and an exp after 2050 to avoid
+    // invalid-timestamp errors from the token server.
+    final long expiresAt = JSONWebTokenUtils.DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS;
+    String assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience, issuer, null, expiresAt);
     if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
       return assertion;
     }
 
     try {
       FxAccountConstants.pii(LOG_TAG, "Generated assertion: " + assertion);
       ExtendedJSONObject a = JSONWebTokenUtils.parseAssertion(assertion);
       if (a != null) {
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -342,19 +342,19 @@ public class FxAccountSyncAdapter extend
         FxAccountGlobalSession globalSession = null;
         try {
           ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
 
           // We compute skew over time using SkewHandler. This yields an unchanging
           // skew adjustment that the HawkAuthHeaderProvider uses to adjust its
           // timestamps. Eventually we might want this to adapt within the scope of a
           // global session.
-          final SkewHandler tokenServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname);
-          final long tokenServerSkew = tokenServerSkewHandler.getSkewInSeconds();
-          final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false, tokenServerSkew);
+          final SkewHandler storageServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname);
+          final long storageServerSkew = storageServerSkewHandler.getSkewInSeconds();
+          final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false, storageServerSkew);
 
           final Context context = getContext();
           final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle);
 
           globalSession = new FxAccountGlobalSession(token.endpoint, syncConfig, callback, context, extras, clientsDataDelegate);
           globalSession.start();
         } catch (Exception e) {
           callback.handleError(globalSession, e);
@@ -498,21 +498,17 @@ public class FxAccountSyncAdapter extend
           notificationManager.update(context, fxAccount);
           try {
             if (state.getStateLabel() != StateLabel.Married) {
               syncDelegate.handleCannotSync(state);
               return;
             }
 
             final Married married = (Married) state;
-            SkewHandler skewHandler = SkewHandler.getSkewHandlerFromEndpointString(tokenServerEndpoint);
-            final long now = System.currentTimeMillis();
-            final long issuedAtMillis = now + skewHandler.getSkewInMillis();
-            final long assertionDurationMillis = this.getAssertionDurationInMilliseconds();
-            final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, issuedAtMillis, assertionDurationMillis);
+            final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER);
 
             /*
              * At this point we're in the correct state to sync, and we're ready to fetch
              * a token and do some work.
              *
              * But first we need to do two things:
              * 1. Check to see whether we're in a backoff situation for the token server.
              *    If we are, but we're not forcing a sync, then we go no further.