1 /*
2  * Copyright (C) 2018 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 com.android.apksig.internal.util;
18 
19 import com.android.apksig.internal.asn1.Asn1BerParser;
20 import com.android.apksig.internal.asn1.Asn1DecodingException;
21 import com.android.apksig.internal.asn1.Asn1DerEncoder;
22 import com.android.apksig.internal.asn1.Asn1EncodingException;
23 import com.android.apksig.internal.x509.Certificate;
24 
25 import java.io.ByteArrayInputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.nio.ByteBuffer;
29 import java.security.cert.CertificateException;
30 import java.security.cert.CertificateFactory;
31 import java.security.cert.X509Certificate;
32 import java.util.ArrayList;
33 import java.util.Base64;
34 import java.util.Collection;
35 
36 /**
37  * Provides methods to generate {@code X509Certificate}s from their encoded form. These methods
38  * can be used to generate certificates that would be rejected by the Java {@code
39  * CertificateFactory}.
40  */
41 public class X509CertificateUtils {
42 
43     private static volatile CertificateFactory sCertFactory = null;
44 
45     // The PEM certificate header and footer as specified in RFC 7468:
46     //   There is exactly one space character (SP) separating the "BEGIN" or
47     //   "END" from the label.  There are exactly five hyphen-minus (also
48     //   known as dash) characters ("-") on both ends of the encapsulation
49     //   boundaries, no more, no less.
50     public static final byte[] BEGIN_CERT_HEADER = "-----BEGIN CERTIFICATE-----".getBytes();
51     public static final byte[] END_CERT_FOOTER = "-----END CERTIFICATE-----".getBytes();
52 
buildCertFactory()53     private static void buildCertFactory() {
54         if (sCertFactory != null) {
55             return;
56         }
57 
58         buildCertFactoryHelper();
59     }
60 
buildCertFactoryHelper()61     private static synchronized void buildCertFactoryHelper() {
62         if (sCertFactory != null) {
63             return;
64         }
65         try {
66             sCertFactory = CertificateFactory.getInstance("X.509");
67         } catch (CertificateException e) {
68             throw new RuntimeException("Failed to create X.509 CertificateFactory", e);
69         }
70     }
71 
72     /**
73      * Generates an {@code X509Certificate} from the {@code InputStream}.
74      *
75      * @throws CertificateException if the {@code InputStream} cannot be decoded to a valid
76      *                              certificate.
77      */
generateCertificate(InputStream in)78     public static X509Certificate generateCertificate(InputStream in) throws CertificateException {
79         byte[] encodedForm;
80         try {
81             encodedForm = ByteStreams.toByteArray(in);
82         } catch (IOException e) {
83             throw new CertificateException("Failed to parse certificate", e);
84         }
85         return generateCertificate(encodedForm);
86     }
87 
88     /**
89      * Generates an {@code X509Certificate} from the encoded form.
90      *
91      * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
92      */
generateCertificate(byte[] encodedForm)93     public static X509Certificate generateCertificate(byte[] encodedForm)
94             throws CertificateException {
95         buildCertFactory();
96         return generateCertificate(encodedForm, sCertFactory);
97     }
98 
99     /**
100      * Generates an {@code X509Certificate} from the encoded form using the provided
101      * {@code CertificateFactory}.
102      *
103      * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
104      */
generateCertificate(byte[] encodedForm, CertificateFactory certFactory)105     public static X509Certificate generateCertificate(byte[] encodedForm,
106             CertificateFactory certFactory) throws CertificateException {
107         X509Certificate certificate;
108         try {
109             certificate = (X509Certificate) certFactory.generateCertificate(
110                     new ByteArrayInputStream(encodedForm));
111             return certificate;
112         } catch (CertificateException e) {
113             // This could be expected if the certificate is encoded using a BER encoding that does
114             // not use the minimum number of bytes to represent the length of the contents; attempt
115             // to decode the certificate using the BER parser and re-encode using the DER encoder
116             // below.
117         }
118         try {
119             // Some apps were previously signed with a BER encoded certificate that now results
120             // in exceptions from the CertificateFactory generateCertificate(s) methods. Since
121             // the original BER encoding of the certificate is used as the signature for these
122             // apps that original encoding must be maintained when signing updated versions of
123             // these apps and any new apps that may require capabilities guarded by the
124             // signature. To maintain the same signature the BER parser can be used to parse
125             // the certificate, then it can be re-encoded to its DER equivalent which is
126             // accepted by the generateCertificate method. The positions in the ByteBuffer can
127             // then be used with the GuaranteedEncodedFormX509Certificate object to ensure the
128             // getEncoded method returns the original signature of the app.
129             ByteBuffer encodedCertBuffer = getNextDEREncodedCertificateBlock(
130                     ByteBuffer.wrap(encodedForm));
131             int startingPos = encodedCertBuffer.position();
132             Certificate reencodedCert = Asn1BerParser.parse(encodedCertBuffer, Certificate.class);
133             byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
134             certificate = (X509Certificate) certFactory.generateCertificate(
135                     new ByteArrayInputStream(reencodedForm));
136             // If the reencodedForm is successfully accepted by the CertificateFactory then copy the
137             // original encoding from the ByteBuffer and use that encoding in the Guaranteed object.
138             byte[] originalEncoding = new byte[encodedCertBuffer.position() - startingPos];
139             encodedCertBuffer.position(startingPos);
140             encodedCertBuffer.get(originalEncoding);
141             GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
142                     new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
143             return guaranteedEncodedCert;
144         } catch (Asn1DecodingException | Asn1EncodingException | CertificateException e) {
145             throw new CertificateException("Failed to parse certificate", e);
146         }
147     }
148 
149     /**
150      * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
151      * InputStream}.
152      *
153      * @throws CertificateException if the InputStream cannot be decoded to zero or more valid
154      *                              {@code Certificate} objects.
155      */
generateCertificates( InputStream in)156     public static Collection<? extends java.security.cert.Certificate> generateCertificates(
157             InputStream in) throws CertificateException {
158         buildCertFactory();
159         return generateCertificates(in, sCertFactory);
160     }
161 
162     /**
163      * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
164      * InputStream} using the provided {@code CertificateFactory}.
165      *
166      * @throws CertificateException if the InputStream cannot be decoded to zero or more valid
167      *                              {@code Certificates} objects.
168      */
generateCertificates( InputStream in, CertificateFactory certFactory)169     public static Collection<? extends java.security.cert.Certificate> generateCertificates(
170             InputStream in, CertificateFactory certFactory) throws CertificateException {
171         // Since the InputStream is not guaranteed to support mark / reset operations first read it
172         // into a byte array to allow using the BER parser / DER encoder if it cannot be read by
173         // the CertificateFactory.
174         byte[] encodedCerts;
175         try {
176             encodedCerts = ByteStreams.toByteArray(in);
177         } catch (IOException e) {
178             throw new CertificateException("Failed to read the input stream", e);
179         }
180         try {
181             return certFactory.generateCertificates(new ByteArrayInputStream(encodedCerts));
182         } catch (CertificateException e) {
183             // This could be expected if the certificates are encoded using a BER encoding that does
184             // not use the minimum number of bytes to represent the length of the contents; attempt
185             // to decode the certificates using the BER parser and re-encode using the DER encoder
186             // below.
187         }
188         try {
189             Collection<X509Certificate> certificates = new ArrayList<>(1);
190             ByteBuffer encodedCertsBuffer = ByteBuffer.wrap(encodedCerts);
191             while (encodedCertsBuffer.hasRemaining()) {
192                 ByteBuffer certBuffer = getNextDEREncodedCertificateBlock(encodedCertsBuffer);
193                 int startingPos = certBuffer.position();
194                 Certificate reencodedCert = Asn1BerParser.parse(certBuffer, Certificate.class);
195                 byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
196                 X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(
197                         new ByteArrayInputStream(reencodedForm));
198                 byte[] originalEncoding = new byte[certBuffer.position() - startingPos];
199                 certBuffer.position(startingPos);
200                 certBuffer.get(originalEncoding);
201                 GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
202                         new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
203                 certificates.add(guaranteedEncodedCert);
204             }
205             return certificates;
206         } catch (Asn1DecodingException | Asn1EncodingException e) {
207             throw new CertificateException("Failed to parse certificates", e);
208         }
209     }
210 
211     /**
212      * Parses the provided ByteBuffer to obtain the next certificate in DER encoding. If the buffer
213      * does not begin with the PEM certificate header then it is returned with the assumption that
214      * it is already DER encoded. If the buffer does begin with the PEM certificate header then the
215      * certificate data is read from the buffer until the PEM certificate footer is reached; this
216      * data is then base64 decoded and returned in a new ByteBuffer.
217      *
218      * If the buffer is in PEM format then the position of the buffer is moved to the end of the
219      * current certificate; if the buffer is already DER encoded then the position of the buffer is
220      * not modified.
221      *
222      * @throws CertificateException if the buffer contains the PEM certificate header but does not
223      *                              contain the expected footer.
224      */
getNextDEREncodedCertificateBlock(ByteBuffer certificateBuffer)225     private static ByteBuffer getNextDEREncodedCertificateBlock(ByteBuffer certificateBuffer)
226             throws CertificateException {
227         if (certificateBuffer == null) {
228             throw new NullPointerException("The certificateBuffer cannot be null");
229         }
230         // if the buffer does not contain enough data for the PEM cert header then just return the
231         // provided buffer.
232         if (certificateBuffer.remaining() < BEGIN_CERT_HEADER.length) {
233             return certificateBuffer;
234         }
235         certificateBuffer.mark();
236         for (int i = 0; i < BEGIN_CERT_HEADER.length; i++) {
237             if (certificateBuffer.get() != BEGIN_CERT_HEADER[i]) {
238                 certificateBuffer.reset();
239                 return certificateBuffer;
240             }
241         }
242         StringBuilder pemEncoding = new StringBuilder();
243         while (certificateBuffer.hasRemaining()) {
244             char encodedChar = (char) certificateBuffer.get();
245             // if the current character is a '-' then the beginning of the footer has been reached
246             if (encodedChar == '-') {
247                 break;
248             } else if (Character.isWhitespace(encodedChar)) {
249                 continue;
250             } else {
251                 pemEncoding.append(encodedChar);
252             }
253         }
254         // start from the second index in the certificate footer since the first '-' should have
255         // been consumed above.
256         for (int i = 1; i < END_CERT_FOOTER.length; i++) {
257             if (!certificateBuffer.hasRemaining()) {
258                 throw new CertificateException(
259                         "The provided input contains the PEM certificate header but does not "
260                                 + "contain sufficient data for the footer");
261             }
262             if (certificateBuffer.get() != END_CERT_FOOTER[i]) {
263                 throw new CertificateException(
264                         "The provided input contains the PEM certificate header without a "
265                                 + "valid certificate footer");
266             }
267         }
268         byte[] derEncoding = Base64.getDecoder().decode(pemEncoding.toString());
269         // consume any trailing whitespace in the byte buffer
270         int nextEncodedChar = certificateBuffer.position();
271         while (certificateBuffer.hasRemaining()) {
272             char trailingChar = (char) certificateBuffer.get();
273             if (Character.isWhitespace(trailingChar)) {
274                 nextEncodedChar++;
275             } else {
276                 break;
277             }
278         }
279         certificateBuffer.position(nextEncodedChar);
280         return ByteBuffer.wrap(derEncoding);
281     }
282 }
283