1 /* 2 * Copyright (c) 2021-2023 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package com.ohos.hapsigntool.hap.sign; 17 18 import com.ohos.hapsigntool.entity.ContentDigestAlgorithm; 19 import com.ohos.hapsigntool.entity.Options; 20 import com.ohos.hapsigntool.entity.SignatureAlgorithm; 21 import com.ohos.hapsigntool.hap.config.SignerConfig; 22 import com.ohos.hapsigntool.entity.Pair; 23 import com.ohos.hapsigntool.hap.entity.SigningBlock; 24 import com.ohos.hapsigntool.error.HapFormatException; 25 import com.ohos.hapsigntool.error.SignatureException; 26 import com.ohos.hapsigntool.utils.FileUtils; 27 import com.ohos.hapsigntool.hap.utils.HapUtils; 28 import com.ohos.hapsigntool.utils.StringUtils; 29 import com.ohos.hapsigntool.zip.ZipDataInput; 30 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.nio.ByteBuffer; 34 import java.nio.ByteOrder; 35 import java.security.DigestException; 36 import java.util.ArrayList; 37 import java.util.HashMap; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Set; 42 import java.util.jar.JarEntry; 43 import java.util.jar.JarFile; 44 import java.util.jar.JarOutputStream; 45 import java.util.stream.Collectors; 46 47 /** 48 * 49 * Hap Signature Scheme signer 50 * 51 * @since 2021/12/21 52 */ 53 public abstract class SignHap { 54 private static final int STORED_ENTRY_SO_ALIGNMENT = 4096; 55 private static final int BUFFER_LENGTH = 4096; 56 private static final int BLOCK_COUNT = 4; 57 private static final int BLOCK_MAGIC = 16; 58 private static final int BLOCK_VERSION = 4; 59 private static final long INIT_OFFSET_LEN = 4L; 60 private static final int OPTIONAL_TYPE_SIZE = 4; 61 private static final int OPTIONAL_LENGTH_SIZE = 4; 62 private static final int OPTIONAL_OFFSET_SIZE = 4; 63 SignHap()64 private SignHap() {} 65 66 /** 67 * Copy the jar file and align the storage entries. 68 * 69 * @param in input hap-file which is opened as a jar-file. 70 * @param out output stream of jar. 71 * @param timestamp ZIP file timestamps 72 * @param defaultAlignment default value of alignment. 73 * @throws IOException io error. 74 * @throws HapFormatException hap format error. 75 */ copyFiles(JarFile in, JarOutputStream out, long timestamp, int defaultAlignment)76 public static void copyFiles(JarFile in, 77 JarOutputStream out, long timestamp, int defaultAlignment) throws IOException, HapFormatException { 78 // split compressed and uncompressed 79 List<JarEntry> entryListStored = in.stream() 80 .filter(jarFile -> jarFile.getMethod() == JarEntry.STORED).collect(Collectors.toList()); 81 82 // uncompressed special files and place in front 83 entryListStored = storedEntryListOfSort(entryListStored); 84 long offset = INIT_OFFSET_LEN; 85 String lastAlignmentEntryName = ""; 86 for (JarEntry inEntry : entryListStored) { 87 String entryName = inEntry.getName(); 88 if (!FileUtils.isRunnableFile(entryName)) { 89 lastAlignmentEntryName = entryName; 90 break; 91 } 92 } 93 for (JarEntry inEntry : entryListStored) { 94 if (inEntry == null) { 95 continue; 96 } 97 98 offset += JarFile.LOCHDR; 99 100 JarEntry outEntry = getJarEntry(timestamp, inEntry); 101 offset += outEntry.getName().length(); 102 103 int alignment = getStoredEntryDataAlignment(inEntry.getName(), defaultAlignment, lastAlignmentEntryName); 104 if (alignment > 0 && (offset % alignment != 0)) { 105 int needed = alignment - (int) (offset % alignment); 106 outEntry.setExtra(new byte[needed]); 107 offset += needed; 108 } 109 110 out.putNextEntry(outEntry); 111 offset = writeOutputStreamAndGetOffset(in, out, inEntry, offset); 112 } 113 List<JarEntry> entryListNotStored = in.stream() 114 .filter(jarFile -> jarFile.getMethod() != JarEntry.STORED).collect(Collectors.toList()); 115 // process byte alignment of the first compressed file 116 boolean isAlignmentFlag = StringUtils.isEmpty(lastAlignmentEntryName); 117 if (isAlignmentFlag) { 118 if (entryListNotStored.isEmpty()) { 119 throw new HapFormatException("Hap format is error, file missing"); 120 } 121 JarEntry firstEntry = entryListNotStored.get(0); 122 offset += JarFile.LOCHDR; 123 JarEntry outEntry = getFirstJarEntry(firstEntry, offset, timestamp); 124 out.putNextEntry(outEntry); 125 byte[] buffer = new byte[BUFFER_LENGTH]; 126 writeOutputStream(in, out, firstEntry, buffer); 127 } 128 129 copyFilesExceptStoredFile(entryListNotStored, in, out, timestamp, isAlignmentFlag); 130 } 131 132 /** 133 * uncompressed special files are placed in front 134 * 135 * @param entryListStored stored file entry list 136 * @return List<JarEntry> jarEntryList 137 */ storedEntryListOfSort(List<JarEntry> entryListStored)138 private static List<JarEntry> storedEntryListOfSort(List<JarEntry> entryListStored) { 139 return entryListStored.stream().sorted((entry1, entry2) -> { 140 String name1 = entry1.getName(); 141 String name2 = entry2.getName(); 142 // files ending with .abc or .so are placed before other files 143 boolean isSpecial1 = FileUtils.isRunnableFile(name1); 144 boolean isSpecial2 = FileUtils.isRunnableFile(name2); 145 if (isSpecial1 && !isSpecial2) { 146 return -1; 147 } else if (!isSpecial1 && isSpecial2) { 148 return 1; 149 } else { 150 // if all files are special files or none of them are special files,the files are sorted lexically 151 return name1.compareTo(name2); 152 } 153 }).collect(Collectors.toList()); 154 } 155 getFirstJarEntry(JarEntry firstEntry, long offset, long timestamp)156 private static JarEntry getFirstJarEntry(JarEntry firstEntry, long offset, long timestamp) { 157 long currentOffset = offset; 158 JarEntry outEntry = getJarEntry(timestamp, firstEntry); 159 currentOffset += outEntry.getName().length(); 160 if (currentOffset % STORED_ENTRY_SO_ALIGNMENT != 0) { 161 int needed = STORED_ENTRY_SO_ALIGNMENT - (int) (currentOffset % STORED_ENTRY_SO_ALIGNMENT); 162 outEntry.setExtra(new byte[needed]); 163 } 164 return outEntry; 165 } 166 167 /** 168 * write first not stored entry to outputStream 169 * 170 * @param in jar file 171 * @param out jarOutputStream 172 * @param firstEntry jarEntry 173 * @param buffer byte[] 174 * @throws IOException IOExpcetion 175 */ writeOutputStream(JarFile in, JarOutputStream out, JarEntry firstEntry, byte[] buffer)176 private static void writeOutputStream(JarFile in, JarOutputStream out, JarEntry firstEntry, byte[] buffer) 177 throws IOException { 178 try (InputStream data = in.getInputStream(firstEntry)) { 179 int num; 180 while ((num = data.read(buffer)) > 0) { 181 out.write(buffer, 0, num); 182 } 183 out.flush(); 184 } 185 } 186 writeOutputStreamAndGetOffset(JarFile in, JarOutputStream out, JarEntry inEntry, long offset)187 private static long writeOutputStreamAndGetOffset(JarFile in, JarOutputStream out, JarEntry inEntry, long offset) 188 throws IOException { 189 byte[] buffer = new byte[BUFFER_LENGTH]; 190 long currentOffset = offset; 191 try (InputStream data = in.getInputStream(inEntry)) { 192 int num; 193 while ((num = data.read(buffer)) > 0) { 194 out.write(buffer, 0, num); 195 currentOffset += num; 196 } 197 out.flush(); 198 } 199 return currentOffset; 200 } 201 getJarEntry(long timestamp, JarEntry inEntry)202 private static JarEntry getJarEntry(long timestamp, JarEntry inEntry) { 203 JarEntry outEntry = new JarEntry(inEntry); 204 outEntry.setTime(timestamp); 205 206 outEntry.setComment(null); 207 outEntry.setExtra(null); 208 return outEntry; 209 } 210 copyFilesExceptStoredFile(List<JarEntry> entryListNotStored, JarFile in, JarOutputStream out, long timestamp, boolean isAlignmentFlag)211 private static void copyFilesExceptStoredFile(List<JarEntry> entryListNotStored, JarFile in, 212 JarOutputStream out, long timestamp, boolean isAlignmentFlag) throws IOException { 213 byte[] buffer = new byte[BUFFER_LENGTH]; 214 int index = 0; 215 if (isAlignmentFlag) { 216 index = 1; 217 } 218 for (; index < entryListNotStored.size(); index++) { 219 JarEntry inEntry = entryListNotStored.get(index); 220 if (inEntry == null || inEntry.getMethod() == JarEntry.STORED) { 221 continue; 222 } 223 224 JarEntry outEntry = new JarEntry(inEntry.getName()); 225 outEntry.setTime(timestamp); 226 out.putNextEntry(outEntry); 227 writeOutputStream(in, out, inEntry, buffer); 228 } 229 } 230 231 /** 232 * If store entry is end with '.so', use 4096-alignment, otherwise, use default-alignment. 233 * 234 * @param entryName name of entry 235 * @param defaultAlignment default value of alignment. 236 * @param lastAlignmentEntryName lastAlignmentEntryName 237 * @return value of alignment. 238 */ getStoredEntryDataAlignment(String entryName, int defaultAlignment, String lastAlignmentEntryName)239 private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment, 240 String lastAlignmentEntryName) { 241 if (defaultAlignment <= 0) { 242 return 0; 243 } 244 if (!StringUtils.isEmpty(lastAlignmentEntryName) && entryName.equals(lastAlignmentEntryName)) { 245 return STORED_ENTRY_SO_ALIGNMENT; 246 } 247 if (FileUtils.isRunnableFile(entryName)) { 248 return STORED_ENTRY_SO_ALIGNMENT; 249 } 250 return defaultAlignment; 251 } 252 getHapSigningBlock( Set<ContentDigestAlgorithm> contentDigestAlgorithms, List<SigningBlock> optionalBlocks, SignerConfig signerConfig, ZipDataInput[] hapData)253 private static byte[] getHapSigningBlock( 254 Set<ContentDigestAlgorithm> contentDigestAlgorithms, 255 List<SigningBlock> optionalBlocks, 256 SignerConfig signerConfig, 257 ZipDataInput[] hapData) 258 throws SignatureException { 259 /** 260 * Compute digests of Hap contents 261 * Sign the digests and wrap the signature and signer info into the Hap Signing Block 262 */ 263 byte[] hapSignatureBytes = null; 264 try { 265 Map<ContentDigestAlgorithm, byte[]> contentDigests = 266 HapUtils.computeDigests(contentDigestAlgorithms, hapData, optionalBlocks); 267 hapSignatureBytes = generateHapSigningBlock(signerConfig, contentDigests, optionalBlocks); 268 } catch (DigestException | IOException e) { 269 throw new SignatureException("Failed to compute digests of HAP", e); 270 } 271 return hapSignatureBytes; 272 } 273 generateHapSigningBlock( SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests, List<SigningBlock> optionalBlocks)274 private static byte[] generateHapSigningBlock( 275 SignerConfig signerConfig, 276 Map<ContentDigestAlgorithm, byte[]> contentDigests, 277 List<SigningBlock> optionalBlocks) 278 throws SignatureException { 279 byte[] hapSignatureSchemeBlock = generateHapSignatureSchemeBlock(signerConfig, contentDigests); 280 return generateHapSigningBlock(hapSignatureSchemeBlock, optionalBlocks, signerConfig.getCompatibleVersion()); 281 } 282 generateHapSigningBlock(byte[] hapSignatureSchemeBlock, List<SigningBlock> optionalBlocks, int compatibleVersion)283 private static byte[] generateHapSigningBlock(byte[] hapSignatureSchemeBlock, 284 List<SigningBlock> optionalBlocks, int compatibleVersion) { 285 // FORMAT: 286 // Proof-of-Rotation pairs(optional): 287 // uint32:type 288 // uint32:length 289 // uint32:offset 290 // Property pairs(optional): 291 // uint32:type 292 // uint32:length 293 // uint32:offset 294 // Profile capability pairs(optional): 295 // uint32:type 296 // uint32:length 297 // uint32:offset 298 // length bytes : app signing pairs 299 // uint32:type 300 // uint32:length 301 // uint32:offset 302 // repeated ID-value pairs(reserved extensions): 303 // length bytes : Proof-of-Rotation values 304 // length bytes : property values 305 // length bytes : profile capability values 306 // length bytes : signature schema values 307 // uint64: size 308 // uint128: magic 309 // uint32: version 310 long optionalBlockSize = 0L; 311 for (SigningBlock optionalBlock : optionalBlocks) { 312 optionalBlockSize += optionalBlock.getLength(); 313 } 314 long resultSize = 315 ((OPTIONAL_TYPE_SIZE + OPTIONAL_LENGTH_SIZE + OPTIONAL_OFFSET_SIZE) * (optionalBlocks.size() + 1)) 316 + optionalBlockSize // optional pair 317 + hapSignatureSchemeBlock.length // App signing pairs 318 + BLOCK_COUNT // block count 319 + HapUtils.BLOCK_SIZE // size 320 + BLOCK_MAGIC // magic 321 + BLOCK_VERSION; // version 322 if (resultSize > Integer.MAX_VALUE) { 323 throw new IllegalArgumentException("HapSigningBlock out of range : " + resultSize); 324 } 325 ByteBuffer result = ByteBuffer.allocate((int) resultSize); 326 result.order(ByteOrder.LITTLE_ENDIAN); 327 328 Map<Integer, Integer> typeAndOffsetMap = new HashMap<Integer, Integer>(); 329 int currentOffset = ((OPTIONAL_TYPE_SIZE + OPTIONAL_LENGTH_SIZE 330 + OPTIONAL_OFFSET_SIZE) * (optionalBlocks.size() + 1)); 331 int currentOffsetInBlockValue = 0; 332 int blockValueSizes = (int) (optionalBlockSize + hapSignatureSchemeBlock.length); 333 byte[] blockValues = new byte[blockValueSizes]; 334 335 for (SigningBlock optionalBlock : optionalBlocks) { 336 System.arraycopy( 337 optionalBlock.getValue(), 0, blockValues, currentOffsetInBlockValue, optionalBlock.getLength()); 338 typeAndOffsetMap.put(optionalBlock.getType(), currentOffset); 339 currentOffset += optionalBlock.getLength(); 340 currentOffsetInBlockValue += optionalBlock.getLength(); 341 } 342 343 System.arraycopy( 344 hapSignatureSchemeBlock, 0, blockValues, currentOffsetInBlockValue, hapSignatureSchemeBlock.length); 345 typeAndOffsetMap.put(HapUtils.HAP_SIGNATURE_SCHEME_V1_BLOCK_ID, currentOffset); 346 347 extractedResult(optionalBlocks, result, typeAndOffsetMap); 348 result.putInt(HapUtils.HAP_SIGNATURE_SCHEME_V1_BLOCK_ID); // type 349 result.putInt(hapSignatureSchemeBlock.length); // length 350 int offset = typeAndOffsetMap.get(HapUtils.HAP_SIGNATURE_SCHEME_V1_BLOCK_ID); 351 result.putInt(offset); // offset 352 result.put(blockValues); 353 result.putInt(optionalBlocks.size() + 1); // Signing block count 354 result.putLong(resultSize); // length of hap signing block 355 result.put(HapUtils.getHapSigningBlockMagic(compatibleVersion)); // magic 356 result.putInt(HapUtils.getHapSigningBlockVersion(compatibleVersion)); // version 357 return result.array(); 358 } 359 extractedResult(List<SigningBlock> optionalBlocks, ByteBuffer result, Map<Integer, Integer> typeAndOffsetMap)360 private static void extractedResult(List<SigningBlock> optionalBlocks, ByteBuffer result, 361 Map<Integer, Integer> typeAndOffsetMap) { 362 int offset; 363 for (SigningBlock optionalBlock : optionalBlocks) { 364 result.putInt(optionalBlock.getType()); // type 365 result.putInt(optionalBlock.getLength()); // length 366 offset = typeAndOffsetMap.get(optionalBlock.getType()); 367 result.putInt(offset); // offset 368 } 369 } 370 generateHapSignatureSchemeBlock( SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)371 private static byte[] generateHapSignatureSchemeBlock( 372 SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests) throws SignatureException { 373 byte[] signerBlock = null; 374 try { 375 signerBlock = generateSignerBlock(signerConfig, contentDigests); 376 } catch (SignatureException e) { 377 throw new SignatureException("generate SignerBlock failed" 378 + "\nSolutions:" 379 + "\n> The keyAlias parameter is incorrect, please input a correct keyAlias parameter." 380 + "\n> The certificate is incorrect, please check if your certificate matches the key"); 381 } 382 return signerBlock; 383 } 384 generateSignerBlock( SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)385 private static byte[] generateSignerBlock( 386 SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests) throws SignatureException { 387 String mode = signerConfig.getOptions().getString(Options.MODE); 388 if (!("remoteSign".equalsIgnoreCase(mode)) && signerConfig.getCertificates().isEmpty()) { 389 throw new SignatureException("No certificates configured for signer"); 390 } 391 392 List<Pair<Integer, byte[]>> digests = 393 new ArrayList<Pair<Integer, byte[]>>(signerConfig.getSignatureAlgorithms().size()); 394 for (SignatureAlgorithm signatureAlgorithm : signerConfig.getSignatureAlgorithms()) { 395 ContentDigestAlgorithm contentDigestAlgorithm = signatureAlgorithm.getContentDigestAlgorithm(); 396 byte[] contentDigest = contentDigests.get(contentDigestAlgorithm); 397 if (contentDigest == null) { 398 throw new SignatureException( 399 contentDigestAlgorithm.getDigestAlgorithm() 400 + " content digest for " 401 + signatureAlgorithm.getSignatureAlgAndParams().getFirst() 402 + " not computed"); 403 } 404 digests.add(Pair.create(signatureAlgorithm.getId(), contentDigest)); 405 } 406 byte[] unsignedHapDigest = HapUtils.encodeListOfPairsToByteArray(digests); 407 return Pkcs7Generator.BC.generateSignedData(unsignedHapDigest, signerConfig); 408 } 409 410 /** 411 * Signs the provided Hap using Hap Signature Scheme and returns the 412 * signed block as an array of ByteBuffer 413 * 414 * @param contents Hap content before ZIP CD 415 * @param signerConfig signer config 416 * @param optionalBlocks optional blocks 417 * @return signed block 418 * @throws SignatureException if an error occurs when sign hap file. 419 */ sign(ZipDataInput[] contents, SignerConfig signerConfig, List<SigningBlock> optionalBlocks)420 public static byte[] sign(ZipDataInput[] contents, SignerConfig signerConfig, List<SigningBlock> optionalBlocks) 421 throws SignatureException { 422 Set<ContentDigestAlgorithm> contentDigestAlgorithms = new HashSet<ContentDigestAlgorithm>(); 423 for (SignatureAlgorithm signatureAlgorithm : signerConfig.getSignatureAlgorithms()) { 424 contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm()); 425 } 426 return getHapSigningBlock(contentDigestAlgorithms, optionalBlocks, signerConfig, contents); 427 } 428 } 429