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