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