1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.http;
18 
19 import com.android.internal.util.HexDump;
20 
21 import android.content.Context;
22 import android.os.Bundle;
23 import android.text.format.DateFormat;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.widget.TextView;
27 
28 import java.io.ByteArrayInputStream;
29 import java.math.BigInteger;
30 import java.security.MessageDigest;
31 import java.security.NoSuchAlgorithmException;
32 import java.security.cert.Certificate;
33 import java.security.cert.CertificateEncodingException;
34 import java.security.cert.CertificateException;
35 import java.security.cert.CertificateFactory;
36 import java.security.cert.X509Certificate;
37 import java.text.ParseException;
38 import java.text.SimpleDateFormat;
39 import java.util.Date;
40 import java.util.Vector;
41 
42 import com.android.org.bouncycastle.asn1.x509.X509Name;
43 
44 /**
45  * SSL certificate info (certificate details) class
46  */
47 public class SslCertificate {
48 
49     /**
50      * SimpleDateFormat pattern for an ISO 8601 date
51      */
52     private static String ISO_8601_DATE_FORMAT = "yyyy-MM-dd HH:mm:ssZ";
53 
54     /**
55      * Name of the entity this certificate is issued to
56      */
57     private final DName mIssuedTo;
58 
59     /**
60      * Name of the entity this certificate is issued by
61      */
62     private final DName mIssuedBy;
63 
64     /**
65      * Not-before date from the validity period
66      */
67     private final Date mValidNotBefore;
68 
69     /**
70      * Not-after date from the validity period
71      */
72     private final Date mValidNotAfter;
73 
74     /**
75      * The original source certificate, if available.
76      *
77      * TODO If deprecated constructors are removed, this should always
78      * be available, and saveState and restoreState can be simplified
79      * to be unconditional.
80      */
81     private final X509Certificate mX509Certificate;
82 
83     /**
84      * Bundle key names
85      */
86     private static final String ISSUED_TO = "issued-to";
87     private static final String ISSUED_BY = "issued-by";
88     private static final String VALID_NOT_BEFORE = "valid-not-before";
89     private static final String VALID_NOT_AFTER = "valid-not-after";
90     private static final String X509_CERTIFICATE = "x509-certificate";
91 
92     /**
93      * Saves the certificate state to a bundle
94      * @param certificate The SSL certificate to store
95      * @return A bundle with the certificate stored in it or null if fails
96      */
saveState(SslCertificate certificate)97     public static Bundle saveState(SslCertificate certificate) {
98         if (certificate == null) {
99             return null;
100         }
101         Bundle bundle = new Bundle();
102         bundle.putString(ISSUED_TO, certificate.getIssuedTo().getDName());
103         bundle.putString(ISSUED_BY, certificate.getIssuedBy().getDName());
104         bundle.putString(VALID_NOT_BEFORE, certificate.getValidNotBefore());
105         bundle.putString(VALID_NOT_AFTER, certificate.getValidNotAfter());
106         X509Certificate x509Certificate = certificate.mX509Certificate;
107         if (x509Certificate != null) {
108             try {
109                 bundle.putByteArray(X509_CERTIFICATE, x509Certificate.getEncoded());
110             } catch (CertificateEncodingException ignored) {
111             }
112         }
113         return bundle;
114     }
115 
116     /**
117      * Restores the certificate stored in the bundle
118      * @param bundle The bundle with the certificate state stored in it
119      * @return The SSL certificate stored in the bundle or null if fails
120      */
restoreState(Bundle bundle)121     public static SslCertificate restoreState(Bundle bundle) {
122         if (bundle == null) {
123             return null;
124         }
125         X509Certificate x509Certificate;
126         byte[] bytes = bundle.getByteArray(X509_CERTIFICATE);
127         if (bytes == null) {
128             x509Certificate = null;
129         } else {
130             try {
131                 CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
132                 Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
133                 x509Certificate = (X509Certificate) cert;
134             } catch (CertificateException e) {
135                 x509Certificate = null;
136             }
137         }
138         return new SslCertificate(bundle.getString(ISSUED_TO),
139                                   bundle.getString(ISSUED_BY),
140                                   parseDate(bundle.getString(VALID_NOT_BEFORE)),
141                                   parseDate(bundle.getString(VALID_NOT_AFTER)),
142                                   x509Certificate);
143     }
144 
145     /**
146      * Creates a new SSL certificate object
147      * @param issuedTo The entity this certificate is issued to
148      * @param issuedBy The entity that issued this certificate
149      * @param validNotBefore The not-before date from the certificate
150      *     validity period in ISO 8601 format
151      * @param validNotAfter The not-after date from the certificate
152      *     validity period in ISO 8601 format
153      * @deprecated Use {@link #SslCertificate(X509Certificate)}
154      */
155     @Deprecated
SslCertificate( String issuedTo, String issuedBy, String validNotBefore, String validNotAfter)156     public SslCertificate(
157             String issuedTo, String issuedBy, String validNotBefore, String validNotAfter) {
158         this(issuedTo, issuedBy, parseDate(validNotBefore), parseDate(validNotAfter), null);
159     }
160 
161     /**
162      * Creates a new SSL certificate object
163      * @param issuedTo The entity this certificate is issued to
164      * @param issuedBy The entity that issued this certificate
165      * @param validNotBefore The not-before date from the certificate validity period
166      * @param validNotAfter The not-after date from the certificate validity period
167      * @deprecated Use {@link #SslCertificate(X509Certificate)}
168      */
169     @Deprecated
SslCertificate( String issuedTo, String issuedBy, Date validNotBefore, Date validNotAfter)170     public SslCertificate(
171             String issuedTo, String issuedBy, Date validNotBefore, Date validNotAfter) {
172         this(issuedTo, issuedBy, validNotBefore, validNotAfter, null);
173     }
174 
175     /**
176      * Creates a new SSL certificate object from an X509 certificate
177      * @param certificate X509 certificate
178      */
SslCertificate(X509Certificate certificate)179     public SslCertificate(X509Certificate certificate) {
180         this(certificate.getSubjectDN().getName(),
181              certificate.getIssuerDN().getName(),
182              certificate.getNotBefore(),
183              certificate.getNotAfter(),
184              certificate);
185     }
186 
SslCertificate( String issuedTo, String issuedBy, Date validNotBefore, Date validNotAfter, X509Certificate x509Certificate)187     private SslCertificate(
188             String issuedTo, String issuedBy,
189             Date validNotBefore, Date validNotAfter,
190             X509Certificate x509Certificate) {
191         mIssuedTo = new DName(issuedTo);
192         mIssuedBy = new DName(issuedBy);
193         mValidNotBefore = cloneDate(validNotBefore);
194         mValidNotAfter  = cloneDate(validNotAfter);
195         mX509Certificate = x509Certificate;
196     }
197 
198     /**
199      * @return Not-before date from the certificate validity period or
200      * "" if none has been set
201      */
getValidNotBeforeDate()202     public Date getValidNotBeforeDate() {
203         return cloneDate(mValidNotBefore);
204     }
205 
206     /**
207      * @return Not-before date from the certificate validity period in
208      * ISO 8601 format or "" if none has been set
209      *
210      * @deprecated Use {@link #getValidNotBeforeDate()}
211      */
212     @Deprecated
getValidNotBefore()213     public String getValidNotBefore() {
214         return formatDate(mValidNotBefore);
215     }
216 
217     /**
218      * @return Not-after date from the certificate validity period or
219      * "" if none has been set
220      */
getValidNotAfterDate()221     public Date getValidNotAfterDate() {
222         return cloneDate(mValidNotAfter);
223     }
224 
225     /**
226      * @return Not-after date from the certificate validity period in
227      * ISO 8601 format or "" if none has been set
228      *
229      * @deprecated Use {@link #getValidNotAfterDate()}
230      */
231     @Deprecated
getValidNotAfter()232     public String getValidNotAfter() {
233         return formatDate(mValidNotAfter);
234     }
235 
236     /**
237      * @return Issued-to distinguished name or null if none has been set
238      */
getIssuedTo()239     public DName getIssuedTo() {
240         return mIssuedTo;
241     }
242 
243     /**
244      * @return Issued-by distinguished name or null if none has been set
245      */
getIssuedBy()246     public DName getIssuedBy() {
247         return mIssuedBy;
248     }
249 
250     /**
251      * Convenience for UI presentation, not intended as public API.
252      */
getSerialNumber(X509Certificate x509Certificate)253     private static String getSerialNumber(X509Certificate x509Certificate) {
254         if (x509Certificate == null) {
255             return "";
256         }
257         BigInteger serialNumber = x509Certificate.getSerialNumber();
258         if (serialNumber == null) {
259             return "";
260         }
261         return fingerprint(serialNumber.toByteArray());
262     }
263 
264     /**
265      * Convenience for UI presentation, not intended as public API.
266      */
getDigest(X509Certificate x509Certificate, String algorithm)267     private static String getDigest(X509Certificate x509Certificate, String algorithm) {
268         if (x509Certificate == null) {
269             return "";
270         }
271         try {
272             byte[] bytes = x509Certificate.getEncoded();
273             MessageDigest md = MessageDigest.getInstance(algorithm);
274             byte[] digest = md.digest(bytes);
275             return fingerprint(digest);
276         } catch (CertificateEncodingException ignored) {
277             return "";
278         } catch (NoSuchAlgorithmException ignored) {
279             return "";
280         }
281     }
282 
fingerprint(byte[] bytes)283     private static final String fingerprint(byte[] bytes) {
284         if (bytes == null) {
285             return "";
286         }
287         StringBuilder sb = new StringBuilder();
288         for (int i = 0; i < bytes.length; i++) {
289             byte b = bytes[i];
290             HexDump.appendByteAsHex(sb, b, true);
291             if (i+1 != bytes.length) {
292                 sb.append(':');
293             }
294         }
295         return sb.toString();
296     }
297 
298     /**
299      * @return A string representation of this certificate for debugging
300      */
toString()301     public String toString() {
302         return ("Issued to: " + mIssuedTo.getDName() + ";\n"
303                 + "Issued by: " + mIssuedBy.getDName() + ";\n");
304     }
305 
306     /**
307      * Parse an ISO 8601 date converting ParseExceptions to a null result;
308      */
parseDate(String string)309     private static Date parseDate(String string) {
310         try {
311             return new SimpleDateFormat(ISO_8601_DATE_FORMAT).parse(string);
312         } catch (ParseException e) {
313             return null;
314         }
315     }
316 
317     /**
318      * Format a date as an ISO 8601 string, return "" for a null date
319      */
formatDate(Date date)320     private static String formatDate(Date date) {
321         if (date == null) {
322             return "";
323         }
324         return new SimpleDateFormat(ISO_8601_DATE_FORMAT).format(date);
325     }
326 
327     /**
328      * Clone a possibly null Date
329      */
cloneDate(Date date)330     private static Date cloneDate(Date date) {
331         if (date == null) {
332             return null;
333         }
334         return (Date) date.clone();
335     }
336 
337     /**
338      * A distinguished name helper class: a 3-tuple of:
339      * <ul>
340      *   <li>the most specific common name (CN)</li>
341      *   <li>the most specific organization (O)</li>
342      *   <li>the most specific organizational unit (OU)</li>
343      * <ul>
344      */
345     public class DName {
346         /**
347          * Distinguished name (normally includes CN, O, and OU names)
348          */
349         private String mDName;
350 
351         /**
352          * Common-name (CN) component of the name
353          */
354         private String mCName;
355 
356         /**
357          * Organization (O) component of the name
358          */
359         private String mOName;
360 
361         /**
362          * Organizational Unit (OU) component of the name
363          */
364         private String mUName;
365 
366         /**
367          * Creates a new {@code DName} from a string. The attributes
368          * are assumed to come in most significant to least
369          * significant order which is true of human readable values
370          * returned by methods such as {@code X500Principal.getName()}.
371          * Be aware that the underlying sources of distinguished names
372          * such as instances of {@code X509Certificate} are encoded in
373          * least significant to most significant order, so make sure
374          * the value passed here has the expected ordering of
375          * attributes.
376          */
DName(String dName)377         public DName(String dName) {
378             if (dName != null) {
379                 mDName = dName;
380                 try {
381                     X509Name x509Name = new X509Name(dName);
382 
383                     Vector val = x509Name.getValues();
384                     Vector oid = x509Name.getOIDs();
385 
386                     for (int i = 0; i < oid.size(); i++) {
387                         if (oid.elementAt(i).equals(X509Name.CN)) {
388                             if (mCName == null) {
389                                 mCName = (String) val.elementAt(i);
390                             }
391                             continue;
392                         }
393 
394                         if (oid.elementAt(i).equals(X509Name.O)) {
395                             if (mOName == null) {
396                                 mOName = (String) val.elementAt(i);
397                                 continue;
398                             }
399                         }
400 
401                         if (oid.elementAt(i).equals(X509Name.OU)) {
402                             if (mUName == null) {
403                                 mUName = (String) val.elementAt(i);
404                                 continue;
405                             }
406                         }
407                     }
408                 } catch (IllegalArgumentException ex) {
409                     // thrown if there is an error parsing the string
410                 }
411             }
412         }
413 
414         /**
415          * @return The distinguished name (normally includes CN, O, and OU names)
416          */
getDName()417         public String getDName() {
418             return mDName != null ? mDName : "";
419         }
420 
421         /**
422          * @return The most specific Common-name (CN) component of this name
423          */
getCName()424         public String getCName() {
425             return mCName != null ? mCName : "";
426         }
427 
428         /**
429          * @return The most specific Organization (O) component of this name
430          */
getOName()431         public String getOName() {
432             return mOName != null ? mOName : "";
433         }
434 
435         /**
436          * @return The most specific Organizational Unit (OU) component of this name
437          */
getUName()438         public String getUName() {
439             return mUName != null ? mUName : "";
440         }
441     }
442 
443     /**
444      * Inflates the SSL certificate view (helper method).
445      * @return The resultant certificate view with issued-to, issued-by,
446      * issued-on, expires-on, and possibly other fields set.
447      *
448      * @hide Used by Browser and Settings
449      */
inflateCertificateView(Context context)450     public View inflateCertificateView(Context context) {
451         LayoutInflater factory = LayoutInflater.from(context);
452 
453         View certificateView = factory.inflate(
454             com.android.internal.R.layout.ssl_certificate, null);
455 
456         // issued to:
457         SslCertificate.DName issuedTo = getIssuedTo();
458         if (issuedTo != null) {
459             ((TextView) certificateView.findViewById(com.android.internal.R.id.to_common))
460                     .setText(issuedTo.getCName());
461             ((TextView) certificateView.findViewById(com.android.internal.R.id.to_org))
462                     .setText(issuedTo.getOName());
463             ((TextView) certificateView.findViewById(com.android.internal.R.id.to_org_unit))
464                     .setText(issuedTo.getUName());
465         }
466         // serial number:
467         ((TextView) certificateView.findViewById(com.android.internal.R.id.serial_number))
468                 .setText(getSerialNumber(mX509Certificate));
469 
470         // issued by:
471         SslCertificate.DName issuedBy = getIssuedBy();
472         if (issuedBy != null) {
473             ((TextView) certificateView.findViewById(com.android.internal.R.id.by_common))
474                     .setText(issuedBy.getCName());
475             ((TextView) certificateView.findViewById(com.android.internal.R.id.by_org))
476                     .setText(issuedBy.getOName());
477             ((TextView) certificateView.findViewById(com.android.internal.R.id.by_org_unit))
478                     .setText(issuedBy.getUName());
479         }
480 
481         // issued on:
482         String issuedOn = formatCertificateDate(context, getValidNotBeforeDate());
483         ((TextView) certificateView.findViewById(com.android.internal.R.id.issued_on))
484                 .setText(issuedOn);
485 
486         // expires on:
487         String expiresOn = formatCertificateDate(context, getValidNotAfterDate());
488         ((TextView) certificateView.findViewById(com.android.internal.R.id.expires_on))
489                 .setText(expiresOn);
490 
491         // fingerprints:
492         ((TextView) certificateView.findViewById(com.android.internal.R.id.sha256_fingerprint))
493                 .setText(getDigest(mX509Certificate, "SHA256"));
494         ((TextView) certificateView.findViewById(com.android.internal.R.id.sha1_fingerprint))
495                 .setText(getDigest(mX509Certificate, "SHA1"));
496 
497         return certificateView;
498     }
499 
500     /**
501      * Formats the certificate date to a properly localized date string.
502      * @return Properly localized version of the certificate date string and
503      * the "" if it fails to localize.
504      */
formatCertificateDate(Context context, Date certificateDate)505     private String formatCertificateDate(Context context, Date certificateDate) {
506         if (certificateDate == null) {
507             return "";
508         }
509         return DateFormat.getDateFormat(context).format(certificateDate);
510     }
511 }
512