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