|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.browserid; |
|
6 |
|
7 import java.io.IOException; |
|
8 import java.io.UnsupportedEncodingException; |
|
9 import java.security.GeneralSecurityException; |
|
10 import java.util.ArrayList; |
|
11 import java.util.Map; |
|
12 import java.util.TreeMap; |
|
13 |
|
14 import org.json.simple.JSONObject; |
|
15 import org.json.simple.parser.ParseException; |
|
16 import org.mozilla.apache.commons.codec.binary.Base64; |
|
17 import org.mozilla.apache.commons.codec.binary.StringUtils; |
|
18 import org.mozilla.gecko.sync.ExtendedJSONObject; |
|
19 import org.mozilla.gecko.sync.NonObjectJSONException; |
|
20 import org.mozilla.gecko.sync.Utils; |
|
21 |
|
22 /** |
|
23 * Encode and decode JSON Web Tokens. |
|
24 * <p> |
|
25 * Reverse-engineered from the Node.js jwcrypto library at |
|
26 * <a href="https://github.com/mozilla/jwcrypto">https://github.com/mozilla/jwcrypto</a> |
|
27 * and informed by the informal draft standard "JSON Web Token (JWT)" at |
|
28 * <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>. |
|
29 */ |
|
30 public class JSONWebTokenUtils { |
|
31 public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000; |
|
32 public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000; |
|
33 public static final long DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS = 9999999999999L; |
|
34 public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1"; |
|
35 public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1"; |
|
36 |
|
37 public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException { |
|
38 return encode(payload, privateKey, null); |
|
39 } |
|
40 |
|
41 protected static String encode(String payload, SigningPrivateKey privateKey, Map<String, Object> headerFields) throws UnsupportedEncodingException, GeneralSecurityException { |
|
42 ExtendedJSONObject header = new ExtendedJSONObject(); |
|
43 if (headerFields != null) { |
|
44 header.putAll(headerFields); |
|
45 } |
|
46 header.put("alg", privateKey.getAlgorithm()); |
|
47 String encodedHeader = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8")); |
|
48 String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8")); |
|
49 ArrayList<String> segments = new ArrayList<String>(); |
|
50 segments.add(encodedHeader); |
|
51 segments.add(encodedPayload); |
|
52 byte[] message = Utils.toDelimitedString(".", segments).getBytes("UTF-8"); |
|
53 byte[] signature = privateKey.signMessage(message); |
|
54 segments.add(Base64.encodeBase64URLSafeString(signature)); |
|
55 return Utils.toDelimitedString(".", segments); |
|
56 } |
|
57 |
|
58 public static String decode(String token, VerifyingPublicKey publicKey) throws GeneralSecurityException, UnsupportedEncodingException { |
|
59 if (token == null) { |
|
60 throw new IllegalArgumentException("token must not be null"); |
|
61 } |
|
62 String[] segments = token.split("\\."); |
|
63 if (segments == null || segments.length != 3) { |
|
64 throw new GeneralSecurityException("malformed token"); |
|
65 } |
|
66 byte[] message = (segments[0] + "." + segments[1]).getBytes("UTF-8"); |
|
67 byte[] signature = Base64.decodeBase64(segments[2]); |
|
68 boolean verifies = publicKey.verifyMessage(message, signature); |
|
69 if (!verifies) { |
|
70 throw new GeneralSecurityException("bad signature"); |
|
71 } |
|
72 String payload = StringUtils.newStringUtf8(Base64.decodeBase64(segments[1])); |
|
73 return payload; |
|
74 } |
|
75 |
|
76 /** |
|
77 * Public for testing. |
|
78 */ |
|
79 @SuppressWarnings("unchecked") |
|
80 public static String getPayloadString(String payloadString, String audience, String issuer, |
|
81 Long issuedAt, long expiresAt) throws NonObjectJSONException, |
|
82 IOException, ParseException { |
|
83 ExtendedJSONObject payload; |
|
84 if (payloadString != null) { |
|
85 payload = new ExtendedJSONObject(payloadString); |
|
86 } else { |
|
87 payload = new ExtendedJSONObject(); |
|
88 } |
|
89 if (audience != null) { |
|
90 payload.put("aud", audience); |
|
91 } |
|
92 payload.put("iss", issuer); |
|
93 if (issuedAt != null) { |
|
94 payload.put("iat", issuedAt); |
|
95 } |
|
96 payload.put("exp", expiresAt); |
|
97 // TreeMap so that keys are sorted. A small attempt to keep output stable over time. |
|
98 return JSONObject.toJSONString(new TreeMap<Object, Object>(payload.object)); |
|
99 } |
|
100 |
|
101 protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException, ParseException { |
|
102 ExtendedJSONObject payload = new ExtendedJSONObject(); |
|
103 ExtendedJSONObject principal = new ExtendedJSONObject(); |
|
104 principal.put("email", email); |
|
105 payload.put("principal", principal); |
|
106 payload.put("public-key", publicKeyToSign.toJSONObject()); |
|
107 return payload.toJSONString(); |
|
108 } |
|
109 |
|
110 public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email, |
|
111 String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException { |
|
112 String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email); |
|
113 String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt); |
|
114 return JSONWebTokenUtils.encode(payloadString, privateKey); |
|
115 } |
|
116 |
|
117 /** |
|
118 * Create a Browser ID assertion. |
|
119 * |
|
120 * @param privateKeyToSignWith |
|
121 * private key to sign assertion with. |
|
122 * @param certificate |
|
123 * to include in assertion; no attempt is made to ensure the |
|
124 * certificate is valid, or corresponds to the private key, or any |
|
125 * other condition. |
|
126 * @param audience |
|
127 * to produce assertion for. |
|
128 * @param issuer |
|
129 * to produce assertion for. |
|
130 * @param issuedAt |
|
131 * timestamp for assertion, in milliseconds since the epoch; if null, |
|
132 * no timestamp is included. |
|
133 * @param expiresAt |
|
134 * expiration timestamp for assertion, in milliseconds since the epoch. |
|
135 * @return assertion. |
|
136 * @throws NonObjectJSONException |
|
137 * @throws IOException |
|
138 * @throws ParseException |
|
139 * @throws GeneralSecurityException |
|
140 */ |
|
141 public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience, |
|
142 String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException { |
|
143 String emptyAssertionPayloadString = "{}"; |
|
144 String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt); |
|
145 String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith); |
|
146 return certificate + "~" + signature; |
|
147 } |
|
148 |
|
149 /** |
|
150 * For debugging only! |
|
151 * |
|
152 * @param input |
|
153 * certificate to dump. |
|
154 * @return non-null object with keys header, payload, signature if the |
|
155 * certificate is well-formed. |
|
156 */ |
|
157 public static ExtendedJSONObject parseCertificate(String input) { |
|
158 try { |
|
159 String[] parts = input.split("\\."); |
|
160 if (parts.length != 3) { |
|
161 return null; |
|
162 } |
|
163 String cHeader = new String(Base64.decodeBase64(parts[0])); |
|
164 String cPayload = new String(Base64.decodeBase64(parts[1])); |
|
165 String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2])); |
|
166 ExtendedJSONObject o = new ExtendedJSONObject(); |
|
167 o.put("header", new ExtendedJSONObject(cHeader)); |
|
168 o.put("payload", new ExtendedJSONObject(cPayload)); |
|
169 o.put("signature", cSignature); |
|
170 return o; |
|
171 } catch (Exception e) { |
|
172 return null; |
|
173 } |
|
174 } |
|
175 |
|
176 /** |
|
177 * For debugging only! |
|
178 * |
|
179 * @param input certificate to dump. |
|
180 * @return true if the certificate is well-formed. |
|
181 */ |
|
182 public static boolean dumpCertificate(String input) { |
|
183 ExtendedJSONObject c = parseCertificate(input); |
|
184 try { |
|
185 if (c == null) { |
|
186 System.out.println("Malformed certificate -- got exception trying to dump contents."); |
|
187 return false; |
|
188 } |
|
189 System.out.println("certificate header: " + c.getString("header")); |
|
190 System.out.println("certificate payload: " + c.getString("payload")); |
|
191 System.out.println("certificate signature: " + c.getString("signature")); |
|
192 return true; |
|
193 } catch (Exception e) { |
|
194 System.out.println("Malformed certificate -- got exception trying to dump contents."); |
|
195 return false; |
|
196 } |
|
197 } |
|
198 |
|
199 /** |
|
200 * For debugging only! |
|
201 * |
|
202 * @param input assertion to dump. |
|
203 * @return true if the assertion is well-formed. |
|
204 */ |
|
205 public static ExtendedJSONObject parseAssertion(String input) { |
|
206 try { |
|
207 String[] parts = input.split("~"); |
|
208 if (parts.length != 2) { |
|
209 return null; |
|
210 } |
|
211 String certificate = parts[0]; |
|
212 String assertion = parts[1]; |
|
213 parts = assertion.split("\\."); |
|
214 if (parts.length != 3) { |
|
215 return null; |
|
216 } |
|
217 String aHeader = new String(Base64.decodeBase64(parts[0])); |
|
218 String aPayload = new String(Base64.decodeBase64(parts[1])); |
|
219 String aSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2])); |
|
220 // We do all the assertion parsing *before* dumping the certificate in |
|
221 // case there's a malformed assertion. |
|
222 ExtendedJSONObject o = new ExtendedJSONObject(); |
|
223 o.put("header", new ExtendedJSONObject(aHeader)); |
|
224 o.put("payload", new ExtendedJSONObject(aPayload)); |
|
225 o.put("signature", aSignature); |
|
226 o.put("certificate", certificate); |
|
227 return o; |
|
228 } catch (Exception e) { |
|
229 return null; |
|
230 } |
|
231 } |
|
232 |
|
233 /** |
|
234 * For debugging only! |
|
235 * |
|
236 * @param input assertion to dump. |
|
237 * @return true if the assertion is well-formed. |
|
238 */ |
|
239 public static boolean dumpAssertion(String input) { |
|
240 ExtendedJSONObject a = parseAssertion(input); |
|
241 try { |
|
242 if (a == null) { |
|
243 System.out.println("Malformed assertion -- got exception trying to dump contents."); |
|
244 return false; |
|
245 } |
|
246 dumpCertificate(a.getString("certificate")); |
|
247 System.out.println("assertion header: " + a.getString("header")); |
|
248 System.out.println("assertion payload: " + a.getString("payload")); |
|
249 System.out.println("assertion signature: " + a.getString("signature")); |
|
250 return true; |
|
251 } catch (Exception e) { |
|
252 System.out.println("Malformed assertion -- got exception trying to dump contents."); |
|
253 return false; |
|
254 } |
|
255 } |
|
256 } |