/* * Copyright (C) 2016 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 com.android.apksig.internal.apk.v2; import com.android.apksig.internal.util.MessageDigestSink; import com.android.apksig.internal.util.Pair; import com.android.apksig.internal.zip.ZipUtils; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.DigestException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * APK Signature Scheme v2 signer. * * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and * uncompressed contents of ZIP entries. * * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a> */ public abstract class V2SchemeSigner { /* * The two main goals of APK Signature Scheme v2 are: * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature * cover every byte of the APK being signed. * 2. Enable much faster signature and integrity verification. This is achieved by requiring * only a minimal amount of APK parsing before the signature is verified, thus completely * bypassing ZIP entry decompression and by making integrity verification parallelizable by * employing a hash tree. * * The generated signature block is wrapped into an APK Signing Block and inserted into the * original APK immediately before the start of ZIP Central Directory. This is to ensure that * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for * extensibility. For example, a future signature scheme could insert its signatures there as * well. The contract of the APK Signing Block is that all contents outside of the block must be * protected by signatures inside the block. */ private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; private static final byte[] APK_SIGNING_BLOCK_MAGIC = new byte[] { 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, }; private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; /** * Signer configuration. */ public static class SignerConfig { /** Private key. */ public PrivateKey privateKey; /** * Certificates, with the first certificate containing the public key corresponding to * {@link #privateKey}. */ public List<X509Certificate> certificates; /** * List of signature algorithms with which to sign. */ public List<SignatureAlgorithm> signatureAlgorithms; } /** Hidden constructor to prevent instantiation. */ private V2SchemeSigner() {} /** * Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the * provided key. * * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see * AndroidManifest.xml minSdkVersion attribute). * * @throws InvalidKeyException if the provided key is not suitable for signing APKs using * APK Signature Scheme v2 */ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms( PublicKey signingKey, int minSdkVersion) throws InvalidKeyException { String keyAlgorithm = signingKey.getAlgorithm(); if ("RSA".equalsIgnoreCase(keyAlgorithm)) { // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee // deterministic signatures which make life easier for OTA updates (fewer files // changed when deterministic signature schemes are used). // Pick a digest which is no weaker than the key. int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength(); if (modulusLengthBits <= 3072) { // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit. return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256); } else { // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the // digest being the weak link. SHA-512 is the next strongest supported digest. return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512); } } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { // DSA is supported only with SHA-256. return Collections.singletonList(SignatureAlgorithm.DSA_WITH_SHA256); } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { // Pick a digest which is no weaker than the key. int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength(); if (keySizeBits <= 256) { // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit. return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA256); } else { // Keys longer than 256 bit need to be paired with a stronger digest to avoid the // digest being the weak link. SHA-512 is the next strongest supported digest. return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512); } } else { throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); } } /** * Signs the provided APK using APK Signature Scheme v2 and returns the APK Signing Block * containing the signature. * * @param signerConfigs signer configurations, one for each signer At least one signer config * must be provided. * * @throws IOException if an I/O error occurs * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is * missing * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or * cannot be used in general * @throws SignatureException if an error occurs when computing digests of generating * signatures */ public static byte[] generateApkSigningBlock( DataSource beforeCentralDir, DataSource centralDir, DataSource eocd, List<SignerConfig> signerConfigs) throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { if (signerConfigs.isEmpty()) { throw new IllegalArgumentException( "No signer configs provided. At least one is required"); } // Figure out which digest(s) to use for APK contents. Set<ContentDigestAlgorithm> contentDigestAlgorithms = new HashSet<>(1); for (SignerConfig signerConfig : signerConfigs) { for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm()); } } // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory // offset field is treated as pointing to the offset at which the APK Signing Block will // start. long centralDirOffsetForDigesting = beforeCentralDir.size(); ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size()); eocdBuf.order(ByteOrder.LITTLE_ENDIAN); eocd.copyTo(0, (int) eocd.size(), eocdBuf); eocdBuf.flip(); ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting); // Compute digests of APK contents. Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest try { contentDigests = computeContentDigests( contentDigestAlgorithms, new DataSource[] { beforeCentralDir, centralDir, DataSources.asDataSource(eocdBuf)}); } catch (IOException e) { throw new IOException("Failed to read APK being signed", e); } catch (DigestException e) { throw new SignatureException("Failed to compute digests of APK", e); } // Sign the digests and wrap the signatures and signer info into an APK Signing Block. return generateApkSigningBlock(signerConfigs, contentDigests); } static Map<ContentDigestAlgorithm, byte[]> computeContentDigests( Set<ContentDigestAlgorithm> digestAlgorithms, DataSource[] contents) throws IOException, NoSuchAlgorithmException, DigestException { // For each digest algorithm the result is computed as follows: // 1. Each segment of contents is split into consecutive chunks of 1 MB in size. // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB. // No chunks are produced for empty (zero length) segments. // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's // length in bytes (uint32 little-endian) and the chunk's contents. // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of // chunks (uint32 little-endian) and the concatenation of digests of chunks of all // segments in-order. long chunkCountLong = 0; for (DataSource input : contents) { chunkCountLong += getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); } if (chunkCountLong > Integer.MAX_VALUE) { throw new DigestException("Input too long: " + chunkCountLong + " chunks"); } int chunkCount = (int) chunkCountLong; ContentDigestAlgorithm[] digestAlgorithmsArray = digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]); MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length]; byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][]; int[] digestOutputSizes = new int[digestAlgorithmsArray.length]; for (int i = 0; i < digestAlgorithmsArray.length; i++) { ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i]; int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes(); digestOutputSizes[i] = digestOutputSizeBytes; byte[] concatenationOfChunkCountAndChunkDigests = new byte[5 + chunkCount * digestOutputSizeBytes]; concatenationOfChunkCountAndChunkDigests[0] = 0x5a; setUnsignedInt32LittleEndian( chunkCount, concatenationOfChunkCountAndChunkDigests, 1); digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests; String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm(); mds[i] = MessageDigest.getInstance(jcaAlgorithm); } MessageDigestSink mdSink = new MessageDigestSink(mds); byte[] chunkContentPrefix = new byte[5]; chunkContentPrefix[0] = (byte) 0xa5; int chunkIndex = 0; // Optimization opportunity: digests of chunks can be computed in parallel. However, // determining the number of computations to be performed in parallel is non-trivial. This // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU // cores, load on the system from other threads of execution and other processes, size of // input. // For now, we compute these digests sequentially and thus have the luxury of improving // performance by writing the digest of each chunk into a pre-allocated buffer at exactly // the right position. This avoids unnecessary allocations, copying, and enables the final // digest to be more efficient because it's presented with all of its input in one go. for (DataSource input : contents) { long inputOffset = 0; long inputRemaining = input.size(); while (inputRemaining > 0) { int chunkSize = (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1); for (int i = 0; i < mds.length; i++) { mds[i].update(chunkContentPrefix); } try { input.feed(inputOffset, chunkSize, mdSink); } catch (IOException e) { throw new IOException("Failed to read chunk #" + chunkIndex, e); } for (int i = 0; i < digestAlgorithmsArray.length; i++) { MessageDigest md = mds[i]; byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; int expectedDigestSizeBytes = digestOutputSizes[i]; int actualDigestSizeBytes = md.digest( concatenationOfChunkCountAndChunkDigests, 5 + chunkIndex * expectedDigestSizeBytes, expectedDigestSizeBytes); if (actualDigestSizeBytes != expectedDigestSizeBytes) { throw new RuntimeException( "Unexpected output size of " + md.getAlgorithm() + " digest: " + actualDigestSizeBytes); } } inputOffset += chunkSize; inputRemaining -= chunkSize; chunkIndex++; } } Map<ContentDigestAlgorithm, byte[]> result = new HashMap<>(digestAlgorithmsArray.length); for (int i = 0; i < digestAlgorithmsArray.length; i++) { ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i]; byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; MessageDigest md = mds[i]; byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests); result.put(digestAlgorithm, digest); } return result; } private static final long getChunkCount(long inputSize, int chunkSize) { return (inputSize + chunkSize - 1) / chunkSize; } private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) { result[offset] = (byte) (value & 0xff); result[offset + 1] = (byte) ((value >> 8) & 0xff); result[offset + 2] = (byte) ((value >> 16) & 0xff); result[offset + 3] = (byte) ((value >> 24) & 0xff); } private static byte[] generateApkSigningBlock( List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { byte[] apkSignatureSchemeV2Block = generateApkSignatureSchemeV2Block(signerConfigs, contentDigests); return generateApkSigningBlock(apkSignatureSchemeV2Block); } private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) { // FORMAT: // uint64: size (excluding this field) // repeated ID-value pairs: // uint64: size (excluding this field) // uint32: ID // (size - 4) bytes: value // uint64: size (same as the one above) // uint128: magic int resultSize = 8 // size + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair + 8 // size + 16 // magic ; ByteBuffer result = ByteBuffer.allocate(resultSize); result.order(ByteOrder.LITTLE_ENDIAN); long blockSizeFieldValue = resultSize - 8; result.putLong(blockSizeFieldValue); long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length; result.putLong(pairSizeFieldValue); result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID); result.put(apkSignatureSchemeV2Block); result.putLong(blockSizeFieldValue); result.put(APK_SIGNING_BLOCK_MAGIC); return result.array(); } private static byte[] generateApkSignatureSchemeV2Block( List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { // FORMAT: // * length-prefixed sequence of length-prefixed signer blocks. List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size()); int signerNumber = 0; for (SignerConfig signerConfig : signerConfigs) { signerNumber++; byte[] signerBlock; try { signerBlock = generateSignerBlock(signerConfig, contentDigests); } catch (InvalidKeyException e) { throw new InvalidKeyException("Signer #" + signerNumber + " failed", e); } catch (SignatureException e) { throw new SignatureException("Signer #" + signerNumber + " failed", e); } signerBlocks.add(signerBlock); } return encodeAsSequenceOfLengthPrefixedElements( new byte[][] { encodeAsSequenceOfLengthPrefixedElements(signerBlocks), }); } private static byte[] generateSignerBlock( SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { if (signerConfig.certificates.isEmpty()) { throw new SignatureException("No certificates configured for signer"); } PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); byte[] encodedPublicKey = encodePublicKey(publicKey); V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData(); try { signedData.certificates = encodeCertificates(signerConfig.certificates); } catch (CertificateEncodingException e) { throw new SignatureException("Failed to encode certificates", e); } List<Pair<Integer, byte[]>> digests = new ArrayList<>(signerConfig.signatureAlgorithms.size()); for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { ContentDigestAlgorithm contentDigestAlgorithm = signatureAlgorithm.getContentDigestAlgorithm(); byte[] contentDigest = contentDigests.get(contentDigestAlgorithm); if (contentDigest == null) { throw new RuntimeException( contentDigestAlgorithm + " content digest for " + signatureAlgorithm + " not computed"); } digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest)); } signedData.digests = digests; V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer(); // FORMAT: // * length-prefixed sequence of length-prefixed digests: // * uint32: signature algorithm ID // * length-prefixed bytes: digest of contents // * length-prefixed sequence of certificates: // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). // * length-prefixed sequence of length-prefixed additional attributes: // * uint32: ID // * (length - 4) bytes: value signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] { encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests), encodeAsSequenceOfLengthPrefixedElements(signedData.certificates), // additional attributes new byte[0], }); signer.publicKey = encodedPublicKey; signer.signatures = new ArrayList<>(signerConfig.signatureAlgorithms.size()); for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { Pair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams = signatureAlgorithm.getJcaSignatureAlgorithmAndParams(); String jcaSignatureAlgorithm = sigAlgAndParams.getFirst(); AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond(); byte[] signatureBytes; try { Signature signature = Signature.getInstance(jcaSignatureAlgorithm); signature.initSign(signerConfig.privateKey); if (jcaSignatureAlgorithmParams != null) { signature.setParameter(jcaSignatureAlgorithmParams); } signature.update(signer.signedData); signatureBytes = signature.sign(); } catch (InvalidKeyException e) { throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e); } catch (InvalidAlgorithmParameterException | SignatureException e) { throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e); } try { Signature signature = Signature.getInstance(jcaSignatureAlgorithm); signature.initVerify(publicKey); if (jcaSignatureAlgorithmParams != null) { signature.setParameter(jcaSignatureAlgorithmParams); } signature.update(signer.signedData); if (!signature.verify(signatureBytes)) { throw new SignatureException("Signature did not verify"); } } catch (InvalidKeyException e) { throw new InvalidKeyException( "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" + " public key from certificate", e); } catch (InvalidAlgorithmParameterException | SignatureException e) { throw new SignatureException( "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" + " public key from certificate", e); } signer.signatures.add(Pair.of(signatureAlgorithm.getId(), signatureBytes)); } // FORMAT: // * length-prefixed signed data // * length-prefixed sequence of length-prefixed signatures: // * uint32: signature algorithm ID // * length-prefixed bytes: signature of signed data // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded) return encodeAsSequenceOfLengthPrefixedElements( new byte[][] { signer.signedData, encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( signer.signatures), signer.publicKey, }); } private static final class V2SignatureSchemeBlock { private static final class Signer { public byte[] signedData; public List<Pair<Integer, byte[]>> signatures; public byte[] publicKey; } private static final class SignedData { public List<Pair<Integer, byte[]>> digests; public List<byte[]> certificates; } } private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException, NoSuchAlgorithmException { byte[] encodedPublicKey = null; if ("X.509".equals(publicKey.getFormat())) { encodedPublicKey = publicKey.getEncoded(); } if (encodedPublicKey == null) { try { encodedPublicKey = KeyFactory.getInstance(publicKey.getAlgorithm()) .getKeySpec(publicKey, X509EncodedKeySpec.class) .getEncoded(); } catch (InvalidKeySpecException e) { throw new InvalidKeyException( "Failed to obtain X.509 encoded form of public key " + publicKey + " of class " + publicKey.getClass().getName(), e); } } if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) { throw new InvalidKeyException( "Failed to obtain X.509 encoded form of public key " + publicKey + " of class " + publicKey.getClass().getName()); } return encodedPublicKey; } private static List<byte[]> encodeCertificates(List<X509Certificate> certificates) throws CertificateEncodingException { List<byte[]> result = new ArrayList<>(certificates.size()); for (X509Certificate certificate : certificates) { result.add(certificate.getEncoded()); } return result; } private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) { return encodeAsSequenceOfLengthPrefixedElements( sequence.toArray(new byte[sequence.size()][])); } private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) { int payloadSize = 0; for (byte[] element : sequence) { payloadSize += 4 + element.length; } ByteBuffer result = ByteBuffer.allocate(payloadSize); result.order(ByteOrder.LITTLE_ENDIAN); for (byte[] element : sequence) { result.putInt(element.length); result.put(element); } return result.array(); } private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( List<Pair<Integer, byte[]>> sequence) { int resultSize = 0; for (Pair<Integer, byte[]> element : sequence) { resultSize += 12 + element.getSecond().length; } ByteBuffer result = ByteBuffer.allocate(resultSize); result.order(ByteOrder.LITTLE_ENDIAN); for (Pair<Integer, byte[]> element : sequence) { byte[] second = element.getSecond(); result.putInt(8 + second.length); result.putInt(element.getFirst()); result.putInt(second.length); result.put(second); } return result.array(); } }