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