1/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
2/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3/* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7"use strict";
8
9
10ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
11ChromeUtils.import("resource://gre/modules/Services.jsm");
12ChromeUtils.import("resource://gre/modules/Log.jsm");
13
14XPCOMUtils.defineLazyServiceGetter(this,
15                                   "IdentityCryptoService",
16                                   "@mozilla.org/identity/crypto-service;1",
17                                   "nsIIdentityCryptoService");
18
19var EXPORTED_SYMBOLS = ["jwcrypto"];
20
21const PREF_LOG_LEVEL = "services.crypto.jwcrypto.log.level";
22
23XPCOMUtils.defineLazyGetter(this, "log", function() {
24  let log = Log.repository.getLogger("Services.Crypto.jwcrypto");
25  // Default log level is "Error", but consumers can change this with the pref
26  // "services.crypto.jwcrypto.log.level".
27  log.level = Log.Level.Error;
28  let appender = new Log.DumpAppender();
29  log.addAppender(appender);
30  try {
31    let level =
32      Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
33      && Services.prefs.getCharPref(PREF_LOG_LEVEL);
34    log.level = Log.Level[level] || Log.Level.Error;
35  } catch (e) {
36    log.error(e);
37  }
38
39  return log;
40});
41
42const ALGORITHMS = { RS256: "RS256", DS160: "DS160" };
43const DURATION_MS = 1000 * 60 * 2; // 2 minutes default assertion lifetime
44
45function generateKeyPair(aAlgorithmName, aCallback) {
46  log.debug("Generate key pair; alg = " + aAlgorithmName);
47
48  IdentityCryptoService.generateKeyPair(aAlgorithmName, function(rv, aKeyPair) {
49    if (!Components.isSuccessCode(rv)) {
50      return aCallback("key generation failed");
51    }
52
53    var publicKey;
54
55    switch (aKeyPair.keyType) {
56     case ALGORITHMS.RS256:
57      publicKey = {
58        algorithm: "RS",
59        exponent:  aKeyPair.hexRSAPublicKeyExponent,
60        modulus:   aKeyPair.hexRSAPublicKeyModulus
61      };
62      break;
63
64     case ALGORITHMS.DS160:
65      publicKey = {
66        algorithm: "DS",
67        y: aKeyPair.hexDSAPublicValue,
68        p: aKeyPair.hexDSAPrime,
69        q: aKeyPair.hexDSASubPrime,
70        g: aKeyPair.hexDSAGenerator
71      };
72      break;
73
74    default:
75      return aCallback("unknown key type");
76    }
77
78    let keyWrapper = {
79      serializedPublicKey: JSON.stringify(publicKey),
80      _kp: aKeyPair
81    };
82
83    return aCallback(null, keyWrapper);
84  });
85}
86
87function sign(aPayload, aKeypair, aCallback) {
88  aKeypair._kp.sign(aPayload, function(rv, signature) {
89    if (!Components.isSuccessCode(rv)) {
90      log.error("signer.sign failed");
91      return aCallback("Sign failed");
92    }
93    log.debug("signer.sign: success");
94    return aCallback(null, signature);
95  });
96}
97
98function jwcryptoClass() {
99}
100
101jwcryptoClass.prototype = {
102  /*
103   * Determine the expiration of the assertion.  Returns expiry date
104   * in milliseconds as integer.
105   *
106   * @param localtimeOffsetMsec (optional)
107   *        The number of milliseconds that must be added to the local clock
108   *        for it to agree with the server.  For example, if the local clock
109   *        if two minutes fast, localtimeOffsetMsec would be -120000
110   *
111   * @param now (options)
112   *        Current date in milliseconds.  Useful for mocking clock
113   *        skew in testing.
114   */
115  getExpiration(duration = DURATION_MS, localtimeOffsetMsec = 0, now = Date.now()) {
116    return now + localtimeOffsetMsec + duration;
117  },
118
119  isCertValid(aCert, aCallback) {
120    // XXX check expiration, bug 769850
121    aCallback(true);
122  },
123
124  generateKeyPair(aAlgorithmName, aCallback) {
125    log.debug("generating");
126    generateKeyPair(aAlgorithmName, aCallback);
127  },
128
129  /*
130   * Generate an assertion and return it through the provided callback.
131   *
132   * @param aCert
133   *        Identity certificate
134   *
135   * @param aKeyPair
136   *        KeyPair object
137   *
138   * @param aAudience
139   *        Audience of the assertion
140   *
141   * @param aOptions (optional)
142   *        Can include:
143   *        {
144   *          localtimeOffsetMsec: <clock offset in milliseconds>,
145   *          now: <current date in milliseconds>
146   *          duration: <validity duration for this assertion in milliseconds>
147   *        }
148   *
149   *        localtimeOffsetMsec is the number of milliseconds that need to be
150   *        added to the local clock time to make it concur with the server.
151   *        For example, if the local clock is two minutes fast, the offset in
152   *        milliseconds would be -120000.
153   *
154   * @param aCallback
155   *        Function to invoke with resulting assertion.  Assertion
156   *        will be string or null on failure.
157   */
158  generateAssertion(aCert, aKeyPair, aAudience, aOptions, aCallback) {
159    if (typeof aOptions == "function") {
160      aCallback = aOptions;
161      aOptions = { };
162    }
163
164    // for now, we hack the algorithm name
165    // XXX bug 769851
166    var header = {"alg": "DS128"};
167    var headerBytes = IdentityCryptoService.base64UrlEncode(
168                          JSON.stringify(header));
169
170    var payload = {
171      exp: this.getExpiration(
172               aOptions.duration, aOptions.localtimeOffsetMsec, aOptions.now),
173      aud: aAudience
174    };
175    var payloadBytes = IdentityCryptoService.base64UrlEncode(
176                          JSON.stringify(payload));
177
178    log.debug("payload", { payload, payloadBytes });
179    sign(headerBytes + "." + payloadBytes, aKeyPair, function(err, signature) {
180      if (err)
181        return aCallback(err);
182
183      var signedAssertion = headerBytes + "." + payloadBytes + "." + signature;
184      return aCallback(null, aCert + "~" + signedAssertion);
185    });
186  }
187
188};
189
190var jwcrypto = new jwcryptoClass();
191this.jwcrypto.ALGORITHMS = ALGORITHMS;
192