| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.net.http; |
| |
| import android.annotation.Nullable; |
| import android.annotation.UnsupportedAppUsage; |
| import android.content.Context; |
| import android.os.Bundle; |
| import android.text.format.DateFormat; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.widget.TextView; |
| |
| import com.android.internal.util.HexDump; |
| import com.android.org.bouncycastle.asn1.x509.X509Name; |
| |
| import java.io.ByteArrayInputStream; |
| import java.math.BigInteger; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.X509Certificate; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.Date; |
| import java.util.Vector; |
| |
| /** |
| * SSL certificate info (certificate details) class |
| */ |
| public class SslCertificate { |
| |
| /** |
| * SimpleDateFormat pattern for an ISO 8601 date |
| */ |
| private static String ISO_8601_DATE_FORMAT = "yyyy-MM-dd HH:mm:ssZ"; |
| |
| /** |
| * Name of the entity this certificate is issued to |
| */ |
| private final DName mIssuedTo; |
| |
| /** |
| * Name of the entity this certificate is issued by |
| */ |
| private final DName mIssuedBy; |
| |
| /** |
| * Not-before date from the validity period |
| */ |
| private final Date mValidNotBefore; |
| |
| /** |
| * Not-after date from the validity period |
| */ |
| private final Date mValidNotAfter; |
| |
| /** |
| * The original source certificate, if available. |
| * |
| * TODO If deprecated constructors are removed, this should always |
| * be available, and saveState and restoreState can be simplified |
| * to be unconditional. |
| */ |
| @UnsupportedAppUsage |
| private final X509Certificate mX509Certificate; |
| |
| /** |
| * Bundle key names |
| */ |
| private static final String ISSUED_TO = "issued-to"; |
| private static final String ISSUED_BY = "issued-by"; |
| private static final String VALID_NOT_BEFORE = "valid-not-before"; |
| private static final String VALID_NOT_AFTER = "valid-not-after"; |
| private static final String X509_CERTIFICATE = "x509-certificate"; |
| |
| /** |
| * Saves the certificate state to a bundle |
| * @param certificate The SSL certificate to store |
| * @return A bundle with the certificate stored in it or null if fails |
| */ |
| public static Bundle saveState(SslCertificate certificate) { |
| if (certificate == null) { |
| return null; |
| } |
| Bundle bundle = new Bundle(); |
| bundle.putString(ISSUED_TO, certificate.getIssuedTo().getDName()); |
| bundle.putString(ISSUED_BY, certificate.getIssuedBy().getDName()); |
| bundle.putString(VALID_NOT_BEFORE, certificate.getValidNotBefore()); |
| bundle.putString(VALID_NOT_AFTER, certificate.getValidNotAfter()); |
| X509Certificate x509Certificate = certificate.mX509Certificate; |
| if (x509Certificate != null) { |
| try { |
| bundle.putByteArray(X509_CERTIFICATE, x509Certificate.getEncoded()); |
| } catch (CertificateEncodingException ignored) { |
| } |
| } |
| return bundle; |
| } |
| |
| /** |
| * Restores the certificate stored in the bundle |
| * @param bundle The bundle with the certificate state stored in it |
| * @return The SSL certificate stored in the bundle or null if fails |
| */ |
| public static SslCertificate restoreState(Bundle bundle) { |
| if (bundle == null) { |
| return null; |
| } |
| X509Certificate x509Certificate; |
| byte[] bytes = bundle.getByteArray(X509_CERTIFICATE); |
| if (bytes == null) { |
| x509Certificate = null; |
| } else { |
| try { |
| CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); |
| Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes)); |
| x509Certificate = (X509Certificate) cert; |
| } catch (CertificateException e) { |
| x509Certificate = null; |
| } |
| } |
| return new SslCertificate(bundle.getString(ISSUED_TO), |
| bundle.getString(ISSUED_BY), |
| parseDate(bundle.getString(VALID_NOT_BEFORE)), |
| parseDate(bundle.getString(VALID_NOT_AFTER)), |
| x509Certificate); |
| } |
| |
| /** |
| * Creates a new SSL certificate object |
| * @param issuedTo The entity this certificate is issued to |
| * @param issuedBy The entity that issued this certificate |
| * @param validNotBefore The not-before date from the certificate |
| * validity period in ISO 8601 format |
| * @param validNotAfter The not-after date from the certificate |
| * validity period in ISO 8601 format |
| * @deprecated Use {@link #SslCertificate(X509Certificate)} |
| */ |
| @Deprecated |
| public SslCertificate( |
| String issuedTo, String issuedBy, String validNotBefore, String validNotAfter) { |
| this(issuedTo, issuedBy, parseDate(validNotBefore), parseDate(validNotAfter), null); |
| } |
| |
| /** |
| * Creates a new SSL certificate object |
| * @param issuedTo The entity this certificate is issued to |
| * @param issuedBy The entity that issued this certificate |
| * @param validNotBefore The not-before date from the certificate validity period |
| * @param validNotAfter The not-after date from the certificate validity period |
| * @deprecated Use {@link #SslCertificate(X509Certificate)} |
| */ |
| @Deprecated |
| public SslCertificate( |
| String issuedTo, String issuedBy, Date validNotBefore, Date validNotAfter) { |
| this(issuedTo, issuedBy, validNotBefore, validNotAfter, null); |
| } |
| |
| /** |
| * Creates a new SSL certificate object from an X509 certificate |
| * @param certificate X509 certificate |
| */ |
| public SslCertificate(X509Certificate certificate) { |
| this(certificate.getSubjectDN().getName(), |
| certificate.getIssuerDN().getName(), |
| certificate.getNotBefore(), |
| certificate.getNotAfter(), |
| certificate); |
| } |
| |
| private SslCertificate( |
| String issuedTo, String issuedBy, |
| Date validNotBefore, Date validNotAfter, |
| X509Certificate x509Certificate) { |
| mIssuedTo = new DName(issuedTo); |
| mIssuedBy = new DName(issuedBy); |
| mValidNotBefore = cloneDate(validNotBefore); |
| mValidNotAfter = cloneDate(validNotAfter); |
| mX509Certificate = x509Certificate; |
| } |
| |
| /** |
| * @return Not-before date from the certificate validity period or |
| * "" if none has been set |
| */ |
| public Date getValidNotBeforeDate() { |
| return cloneDate(mValidNotBefore); |
| } |
| |
| /** |
| * @return Not-before date from the certificate validity period in |
| * ISO 8601 format or "" if none has been set |
| * |
| * @deprecated Use {@link #getValidNotBeforeDate()} |
| */ |
| @Deprecated |
| public String getValidNotBefore() { |
| return formatDate(mValidNotBefore); |
| } |
| |
| /** |
| * @return Not-after date from the certificate validity period or |
| * "" if none has been set |
| */ |
| public Date getValidNotAfterDate() { |
| return cloneDate(mValidNotAfter); |
| } |
| |
| /** |
| * @return Not-after date from the certificate validity period in |
| * ISO 8601 format or "" if none has been set |
| * |
| * @deprecated Use {@link #getValidNotAfterDate()} |
| */ |
| @Deprecated |
| public String getValidNotAfter() { |
| return formatDate(mValidNotAfter); |
| } |
| |
| /** |
| * @return Issued-to distinguished name or null if none has been set |
| */ |
| public DName getIssuedTo() { |
| return mIssuedTo; |
| } |
| |
| /** |
| * @return Issued-by distinguished name or null if none has been set |
| */ |
| public DName getIssuedBy() { |
| return mIssuedBy; |
| } |
| |
| /** |
| * @return The {@code X509Certificate} used to create this {@code SslCertificate} or |
| * {@code null} if no certificate was provided. |
| */ |
| public @Nullable X509Certificate getX509Certificate() { |
| return mX509Certificate; |
| } |
| |
| /** |
| * Convenience for UI presentation, not intended as public API. |
| */ |
| @UnsupportedAppUsage |
| private static String getSerialNumber(X509Certificate x509Certificate) { |
| if (x509Certificate == null) { |
| return ""; |
| } |
| BigInteger serialNumber = x509Certificate.getSerialNumber(); |
| if (serialNumber == null) { |
| return ""; |
| } |
| return fingerprint(serialNumber.toByteArray()); |
| } |
| |
| /** |
| * Convenience for UI presentation, not intended as public API. |
| */ |
| @UnsupportedAppUsage |
| private static String getDigest(X509Certificate x509Certificate, String algorithm) { |
| if (x509Certificate == null) { |
| return ""; |
| } |
| try { |
| byte[] bytes = x509Certificate.getEncoded(); |
| MessageDigest md = MessageDigest.getInstance(algorithm); |
| byte[] digest = md.digest(bytes); |
| return fingerprint(digest); |
| } catch (CertificateEncodingException ignored) { |
| return ""; |
| } catch (NoSuchAlgorithmException ignored) { |
| return ""; |
| } |
| } |
| |
| private static final String fingerprint(byte[] bytes) { |
| if (bytes == null) { |
| return ""; |
| } |
| StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < bytes.length; i++) { |
| byte b = bytes[i]; |
| HexDump.appendByteAsHex(sb, b, true); |
| if (i+1 != bytes.length) { |
| sb.append(':'); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * @return A string representation of this certificate for debugging |
| */ |
| public String toString() { |
| return ("Issued to: " + mIssuedTo.getDName() + ";\n" |
| + "Issued by: " + mIssuedBy.getDName() + ";\n"); |
| } |
| |
| /** |
| * Parse an ISO 8601 date converting ParseExceptions to a null result; |
| */ |
| private static Date parseDate(String string) { |
| try { |
| return new SimpleDateFormat(ISO_8601_DATE_FORMAT).parse(string); |
| } catch (ParseException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Format a date as an ISO 8601 string, return "" for a null date |
| */ |
| private static String formatDate(Date date) { |
| if (date == null) { |
| return ""; |
| } |
| return new SimpleDateFormat(ISO_8601_DATE_FORMAT).format(date); |
| } |
| |
| /** |
| * Clone a possibly null Date |
| */ |
| private static Date cloneDate(Date date) { |
| if (date == null) { |
| return null; |
| } |
| return (Date) date.clone(); |
| } |
| |
| /** |
| * A distinguished name helper class: a 3-tuple of: |
| * <ul> |
| * <li>the most specific common name (CN)</li> |
| * <li>the most specific organization (O)</li> |
| * <li>the most specific organizational unit (OU)</li> |
| * <ul> |
| */ |
| public class DName { |
| /** |
| * Distinguished name (normally includes CN, O, and OU names) |
| */ |
| private String mDName; |
| |
| /** |
| * Common-name (CN) component of the name |
| */ |
| private String mCName; |
| |
| /** |
| * Organization (O) component of the name |
| */ |
| private String mOName; |
| |
| /** |
| * Organizational Unit (OU) component of the name |
| */ |
| private String mUName; |
| |
| /** |
| * Creates a new {@code DName} from a string. The attributes |
| * are assumed to come in most significant to least |
| * significant order which is true of human readable values |
| * returned by methods such as {@code X500Principal.getName()}. |
| * Be aware that the underlying sources of distinguished names |
| * such as instances of {@code X509Certificate} are encoded in |
| * least significant to most significant order, so make sure |
| * the value passed here has the expected ordering of |
| * attributes. |
| */ |
| public DName(String dName) { |
| if (dName != null) { |
| mDName = dName; |
| try { |
| X509Name x509Name = new X509Name(dName); |
| |
| Vector val = x509Name.getValues(); |
| Vector oid = x509Name.getOIDs(); |
| |
| for (int i = 0; i < oid.size(); i++) { |
| if (oid.elementAt(i).equals(X509Name.CN)) { |
| if (mCName == null) { |
| mCName = (String) val.elementAt(i); |
| } |
| continue; |
| } |
| |
| if (oid.elementAt(i).equals(X509Name.O)) { |
| if (mOName == null) { |
| mOName = (String) val.elementAt(i); |
| continue; |
| } |
| } |
| |
| if (oid.elementAt(i).equals(X509Name.OU)) { |
| if (mUName == null) { |
| mUName = (String) val.elementAt(i); |
| continue; |
| } |
| } |
| } |
| } catch (IllegalArgumentException ex) { |
| // thrown if there is an error parsing the string |
| } |
| } |
| } |
| |
| /** |
| * @return The distinguished name (normally includes CN, O, and OU names) |
| */ |
| public String getDName() { |
| return mDName != null ? mDName : ""; |
| } |
| |
| /** |
| * @return The most specific Common-name (CN) component of this name |
| */ |
| public String getCName() { |
| return mCName != null ? mCName : ""; |
| } |
| |
| /** |
| * @return The most specific Organization (O) component of this name |
| */ |
| public String getOName() { |
| return mOName != null ? mOName : ""; |
| } |
| |
| /** |
| * @return The most specific Organizational Unit (OU) component of this name |
| */ |
| public String getUName() { |
| return mUName != null ? mUName : ""; |
| } |
| } |
| |
| /** |
| * Inflates the SSL certificate view (helper method). |
| * @return The resultant certificate view with issued-to, issued-by, |
| * issued-on, expires-on, and possibly other fields set. |
| * |
| * @hide Used by Browser and Settings |
| */ |
| @UnsupportedAppUsage |
| public View inflateCertificateView(Context context) { |
| LayoutInflater factory = LayoutInflater.from(context); |
| |
| View certificateView = factory.inflate( |
| com.android.internal.R.layout.ssl_certificate, null); |
| |
| // issued to: |
| SslCertificate.DName issuedTo = getIssuedTo(); |
| if (issuedTo != null) { |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.to_common)) |
| .setText(issuedTo.getCName()); |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.to_org)) |
| .setText(issuedTo.getOName()); |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.to_org_unit)) |
| .setText(issuedTo.getUName()); |
| } |
| // serial number: |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.serial_number)) |
| .setText(getSerialNumber(mX509Certificate)); |
| |
| // issued by: |
| SslCertificate.DName issuedBy = getIssuedBy(); |
| if (issuedBy != null) { |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.by_common)) |
| .setText(issuedBy.getCName()); |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.by_org)) |
| .setText(issuedBy.getOName()); |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.by_org_unit)) |
| .setText(issuedBy.getUName()); |
| } |
| |
| // issued on: |
| String issuedOn = formatCertificateDate(context, getValidNotBeforeDate()); |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.issued_on)) |
| .setText(issuedOn); |
| |
| // expires on: |
| String expiresOn = formatCertificateDate(context, getValidNotAfterDate()); |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.expires_on)) |
| .setText(expiresOn); |
| |
| // fingerprints: |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.sha256_fingerprint)) |
| .setText(getDigest(mX509Certificate, "SHA256")); |
| ((TextView) certificateView.findViewById(com.android.internal.R.id.sha1_fingerprint)) |
| .setText(getDigest(mX509Certificate, "SHA1")); |
| |
| return certificateView; |
| } |
| |
| /** |
| * Formats the certificate date to a properly localized date string. |
| * @return Properly localized version of the certificate date string and |
| * the "" if it fails to localize. |
| */ |
| private String formatCertificateDate(Context context, Date certificateDate) { |
| if (certificateDate == null) { |
| return ""; |
| } |
| return DateFormat.getMediumDateFormat(context).format(certificateDate); |
| } |
| } |