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