1 /* 2 * Copyright (c) 2023-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.codesigning.sign; 17 18 import com.ohos.hapsigntool.codesigning.datastructure.CodeSignBlock; 19 import com.ohos.hapsigntool.codesigning.datastructure.ElfSignBlock; 20 import com.ohos.hapsigntool.codesigning.datastructure.Extension; 21 import com.ohos.hapsigntool.codesigning.datastructure.FsVerityInfoSegment; 22 import com.ohos.hapsigntool.codesigning.datastructure.MerkleTreeExtension; 23 import com.ohos.hapsigntool.codesigning.datastructure.SignInfo; 24 import com.ohos.hapsigntool.codesigning.exception.CodeSignException; 25 import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException; 26 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptor; 27 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptorWithSign; 28 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityGenerator; 29 import com.ohos.hapsigntool.codesigning.utils.HapUtils; 30 import com.ohos.hapsigntool.entity.Pair; 31 import com.ohos.hapsigntool.error.HapFormatException; 32 import com.ohos.hapsigntool.error.ProfileException; 33 import com.ohos.hapsigntool.hap.config.SignerConfig; 34 import com.ohos.hapsigntool.signer.LocalSigner; 35 import com.ohos.hapsigntool.utils.FileUtils; 36 import com.ohos.hapsigntool.utils.StringUtils; 37 import com.ohos.hapsigntool.zip.Zip; 38 import com.ohos.hapsigntool.zip.ZipEntry; 39 import com.ohos.hapsigntool.zip.ZipEntryHeader; 40 41 import org.apache.logging.log4j.LogManager; 42 import org.apache.logging.log4j.Logger; 43 44 import java.io.File; 45 import java.io.FileInputStream; 46 import java.io.IOException; 47 import java.io.InputStream; 48 import java.util.ArrayList; 49 import java.util.Enumeration; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Locale; 53 import java.util.Map; 54 import java.util.jar.JarEntry; 55 import java.util.jar.JarFile; 56 import java.util.stream.Collectors; 57 import java.util.zip.ZipInputStream; 58 59 /** 60 * core functions of code signing 61 * 62 * @since 2023/06/05 63 */ 64 public class CodeSigning { 65 /** 66 * Only hap and hsp, hqf bundle supports code signing 67 */ 68 public static final String[] SUPPORT_FILE_FORM = {"hap", "hsp", "hqf"}; 69 70 /** 71 * Only elf file supports bin code signing 72 */ 73 public static final String SUPPORT_BIN_FILE_FORM = "elf"; 74 75 /** 76 * Defined entry name of hap file 77 */ 78 public static final String HAP_SIGNATURE_ENTRY_NAME = "Hap"; 79 80 private static final Logger LOGGER = LogManager.getLogger(CodeSigning.class); 81 82 private static final String NATIVE_LIB_AN_SUFFIX = ".an"; 83 84 private static final String NATIVE_LIB_SO_SUFFIX = ".so"; 85 86 private final SignerConfig signConfig; 87 88 private CodeSignBlock codeSignBlock; 89 90 /** 91 * provide code sign functions to sign a hap 92 * 93 * @param signConfig configuration of sign 94 */ CodeSigning(SignerConfig signConfig)95 public CodeSigning(SignerConfig signConfig) { 96 this.signConfig = signConfig; 97 } 98 99 /** 100 * Sign the given elf file, and pack all signature into output file 101 * 102 * @param input file to sign 103 * @param offset position of codesign block based on start of the file 104 * @param inForm file's format 105 * @param profileContent profile of the elf 106 * @return byte array of code sign block 107 * @throws CodeSignException code signing exception 108 * @throws IOException io error 109 * @throws FsVerityDigestException computing FsVerity digest error 110 * @throws ProfileException profile of elf is invalid 111 */ getElfCodeSignBlock(File input, long offset, String inForm, String profileContent)112 public byte[] getElfCodeSignBlock(File input, long offset, String inForm, String profileContent) 113 throws CodeSignException, FsVerityDigestException, IOException, ProfileException { 114 LOGGER.info("Start to sign code."); 115 if (!SUPPORT_BIN_FILE_FORM.equalsIgnoreCase(inForm)) { 116 throw new CodeSignException("file's format is unsupported"); 117 } 118 long fileSize = input.length(); 119 int paddingSize = ElfSignBlock.computeMerkleTreePaddingLength(offset); 120 long fsvTreeOffset = offset + Integer.BYTES * 2 + paddingSize; 121 try (FileInputStream inputStream = new FileInputStream(input)) { 122 FsVerityGenerator fsVerityGenerator = new FsVerityGenerator(); 123 fsVerityGenerator.generateFsVerityDigest(inputStream, fileSize, fsvTreeOffset); 124 byte[] fsVerityDigest = fsVerityGenerator.getFsVerityDigest(); 125 // ownerID should be DEBUG_LIB_ID while signing ELF 126 String ownerID = (profileContent == null) 127 ? HapUtils.HAP_DEBUG_OWNER_ID 128 : HapUtils.getAppIdentifier(profileContent); 129 byte[] signature = generateSignature(fsVerityDigest, ownerID); 130 // add fs-verify info 131 FsVerityDescriptor.Builder fsdbuilder = new FsVerityDescriptor.Builder().setFileSize(fileSize) 132 .setHashAlgorithm(FsVerityGenerator.getFsVerityHashAlgorithm()) 133 .setLog2BlockSize(FsVerityGenerator.getLog2BlockSize()) 134 .setSaltSize((byte) fsVerityGenerator.getSaltSize()) 135 .setSignSize(signature.length) 136 .setFileSize(fileSize) 137 .setSalt(fsVerityGenerator.getSalt()) 138 .setRawRootHash(fsVerityGenerator.getRootHash()) 139 .setFlags(FsVerityDescriptor.FLAG_STORE_MERKLE_TREE_OFFSET) 140 .setMerkleTreeOffset(fsvTreeOffset) 141 .setCsVersion(FsVerityDescriptor.CODE_SIGN_VERSION); 142 FsVerityDescriptorWithSign fsVerityDescriptorWithSign = new FsVerityDescriptorWithSign(fsdbuilder.build(), 143 signature); 144 byte[] treeBytes = fsVerityGenerator.getTreeBytes(); 145 ElfSignBlock signBlock = new ElfSignBlock(paddingSize, treeBytes, fsVerityDescriptorWithSign); 146 LOGGER.info("Sign elf successfully."); 147 return signBlock.toByteArray(); 148 } 149 } 150 151 /** 152 * Sign the given hap file, and pack all signature into output file 153 * 154 * @param input file to sign 155 * @param offset position of codesign block based on start of the file 156 * @param inForm file's format 157 * @param profileContent profile of the hap 158 * @param zip zip 159 * @return byte array of code sign block 160 * @throws CodeSignException code signing exception 161 * @throws IOException io error 162 * @throws HapFormatException hap format invalid 163 * @throws FsVerityDigestException computing FsVerity digest error 164 * @throws ProfileException profile of the hap error 165 */ getCodeSignBlock(File input, long offset, String inForm, String profileContent, Zip zip)166 public byte[] getCodeSignBlock(File input, long offset, String inForm, String profileContent, Zip zip) 167 throws CodeSignException, IOException, HapFormatException, FsVerityDigestException, ProfileException { 168 LOGGER.info("Start to sign code."); 169 if (!StringUtils.containsIgnoreCase(SUPPORT_FILE_FORM, inForm)) { 170 throw new CodeSignException("file's format is unsupported"); 171 } 172 long dataSize = computeDataSize(zip); 173 // generate CodeSignBlock 174 this.codeSignBlock = new CodeSignBlock(); 175 // compute merkle tree offset, replace with computeMerkleTreeOffset if fs-verity descriptor supports 176 long fsvTreeOffset = this.codeSignBlock.computeMerkleTreeOffset(offset); 177 // update fs-verity segment 178 FsVerityInfoSegment fsVerityInfoSegment = new FsVerityInfoSegment(FsVerityDescriptor.VERSION, 179 FsVerityGenerator.getFsVerityHashAlgorithm(), FsVerityGenerator.getLog2BlockSize()); 180 this.codeSignBlock.setFsVerityInfoSegment(fsVerityInfoSegment); 181 182 LOGGER.debug("Sign hap."); 183 String ownerID = HapUtils.getAppIdentifier(profileContent); 184 185 try (FileInputStream inputStream = new FileInputStream(input)) { 186 Pair<SignInfo, byte[]> hapSignInfoAndMerkleTreeBytesPair = signFile(inputStream, dataSize, true, 187 fsvTreeOffset, ownerID); 188 // update hap segment in CodeSignBlock 189 this.codeSignBlock.getHapInfoSegment().setSignInfo(hapSignInfoAndMerkleTreeBytesPair.getFirst()); 190 // Insert merkle tree bytes into code sign block 191 this.codeSignBlock.addOneMerkleTree(HAP_SIGNATURE_ENTRY_NAME, 192 hapSignInfoAndMerkleTreeBytesPair.getSecond()); 193 } 194 // update native lib info segment in CodeSignBlock 195 List<Pair<String, SignInfo>> nativeLibInfoList = new ArrayList<>(); 196 nativeLibInfoList.addAll(signNativeLibs(input, ownerID)); 197 nativeLibInfoList.addAll(signNativeHnps(input, profileContent, ownerID)); 198 // update SoInfoSegment in CodeSignBlock 199 this.codeSignBlock.getSoInfoSegment().setSoInfoList(nativeLibInfoList); 200 201 // last update codeSignBlock before generating its byte array representation 202 updateCodeSignBlock(this.codeSignBlock); 203 204 // complete code sign block byte array here 205 byte[] generated = this.codeSignBlock.generateCodeSignBlockByte(fsvTreeOffset); 206 LOGGER.info("Sign successfully."); 207 return generated; 208 } 209 computeDataSize(Zip zip)210 private long computeDataSize(Zip zip) throws HapFormatException { 211 long dataSize = 0L; 212 for (ZipEntry entry : zip.getZipEntries()) { 213 ZipEntryHeader zipEntryHeader = entry.getZipEntryData().getZipEntryHeader(); 214 if (FileUtils.isRunnableFile(zipEntryHeader.getFileName()) 215 && zipEntryHeader.getMethod() == Zip.FILE_UNCOMPRESS_METHOD_FLAG) { 216 continue; 217 } 218 // if the first file is not uncompressed abc or so, set dataSize to zero 219 if (entry.getCentralDirectory().getOffset() == 0) { 220 break; 221 } 222 // the first entry which is not abc/so/an is found, return its data offset 223 dataSize = entry.getCentralDirectory().getOffset() + ZipEntryHeader.HEADER_LENGTH 224 + zipEntryHeader.getFileNameLength() + zipEntryHeader.getExtraLength(); 225 break; 226 } 227 if ((dataSize % CodeSignBlock.PAGE_SIZE_4K) != 0) { 228 throw new HapFormatException( 229 String.format(Locale.ROOT, "Invalid dataSize(%d), not a multiple of 4096", dataSize)); 230 } 231 return dataSize; 232 } 233 signNativeLibs(File input, String ownerID)234 private List<Pair<String, SignInfo>> signNativeLibs(File input, String ownerID) 235 throws IOException, FsVerityDigestException, CodeSignException { 236 // sign native files 237 try (JarFile inputJar = new JarFile(input, false)) { 238 List<String> entryNames = getNativeEntriesFromHap(inputJar); 239 if (entryNames.isEmpty()) { 240 LOGGER.info("No native libs."); 241 return new ArrayList<>(); 242 } 243 return signFilesFromJar(entryNames, inputJar, ownerID); 244 } 245 } 246 signNativeHnps(File input, String profileContent, String ownerID)247 private List<Pair<String, SignInfo>> signNativeHnps(File input, String profileContent, String ownerID) 248 throws IOException, CodeSignException, ProfileException { 249 List<Pair<String, SignInfo>> nativeLibInfoList = new ArrayList<>(); 250 try (JarFile inputJar = new JarFile(input, false)) { 251 Map<String, String> hnpTypeMap = HapUtils.getHnpsFromJson(inputJar); 252 // get hnp entry 253 for (Enumeration<JarEntry> e = inputJar.entries(); e.hasMoreElements(); ) { 254 JarEntry entry = e.nextElement(); 255 String entryName = entry.getName(); 256 if (entry.isDirectory() || !entryName.startsWith("hnp/") || !entryName.toLowerCase(Locale.ROOT) 257 .endsWith(".hnp")) { 258 continue; 259 } 260 String hnpFileName = HapUtils.parseHnpPath(entryName); 261 if (!hnpTypeMap.containsKey(hnpFileName)) { 262 throw new CodeSignException("hnp should be described in module.json"); 263 } 264 LOGGER.debug("Sign hnp name = {}", entryName); 265 String type = hnpTypeMap.get(hnpFileName); 266 String hnpOwnerId = ownerID; 267 if ("public".equals(type)) { 268 hnpOwnerId = HapUtils.getPublicHnpOwnerId(profileContent); 269 } 270 nativeLibInfoList.addAll(signHnpLibs(inputJar, entry, hnpOwnerId)); 271 } 272 } 273 return nativeLibInfoList; 274 } 275 signHnpLibs(JarFile inputJar, JarEntry hnpEntry, String ownerID)276 private List<Pair<String, SignInfo>> signHnpLibs(JarFile inputJar, JarEntry hnpEntry, String ownerID) 277 throws IOException, CodeSignException { 278 Map<String, Long> elfEntries = getElfEntriesFromHnp(inputJar, hnpEntry); 279 List<Pair<String, SignInfo>> nativeLibInfoList = elfEntries.entrySet().stream().parallel().map(elf -> { 280 String hnpElfPath = hnpEntry.getName() + "!/" + elf.getKey(); 281 try (InputStream inputStream = inputJar.getInputStream(hnpEntry); 282 ZipInputStream hnpInputStream = new ZipInputStream(inputStream)) { 283 return signHnpElf(hnpInputStream, hnpElfPath, ownerID, elf); 284 } catch (IOException | FsVerityDigestException | CodeSignException e) { 285 LOGGER.error("Sign hnp lib error, entry name = {}, msg : {}", hnpElfPath, e.getMessage()); 286 } 287 return null; 288 }).collect(Collectors.toList()); 289 if (nativeLibInfoList.contains(null)) { 290 throw new CodeSignException("Sign hnp lib error"); 291 } 292 return nativeLibInfoList; 293 } 294 295 /** 296 * sign hnp's elf 297 * 298 * @param hnpInputStream hnp entry input stream 299 * @param hnpElfPath hnp's elf path 300 * @param ownerID ownerId 301 * @param elf elf entry 302 * @return native lib info 303 * @throws IOException io error 304 * @throws FsVerityDigestException computing FsVerity digest error 305 * @throws CodeSignException code signing exception 306 */ signHnpElf(ZipInputStream hnpInputStream, String hnpElfPath, String ownerID, Map.Entry<String, Long> elf)307 public Pair<String, SignInfo> signHnpElf(ZipInputStream hnpInputStream, String hnpElfPath, String ownerID, 308 Map.Entry<String, Long> elf) throws IOException, FsVerityDigestException, CodeSignException { 309 java.util.zip.ZipEntry libEntry = null; 310 while ((libEntry = hnpInputStream.getNextEntry()) != null) { 311 if (elf.getKey().equals(libEntry.getName())) { 312 long fileSize = elf.getValue(); 313 // We don't store merkle tree in code signing of native libs 314 // Therefore, the second value of pair returned is ignored 315 Pair<SignInfo, byte[]> pairSignInfoAndMerkleTreeBytes = signFile(hnpInputStream, fileSize, false, 0, 316 ownerID); 317 return (Pair.create(hnpElfPath, pairSignInfoAndMerkleTreeBytes.getFirst())); 318 } 319 } 320 return null; 321 } 322 getElfEntriesFromHnp(JarFile inputJar, JarEntry hnpEntry)323 private Map<String, Long> getElfEntriesFromHnp(JarFile inputJar, JarEntry hnpEntry) throws IOException { 324 Map<String, Long> elfEntries = new HashMap<>(); 325 try (InputStream inputStream = inputJar.getInputStream(hnpEntry); 326 ZipInputStream hnpInputStream = new ZipInputStream(inputStream)) { 327 java.util.zip.ZipEntry libEntry = null; 328 while ((libEntry = hnpInputStream.getNextEntry()) != null) { 329 byte[] bytes = new byte[4]; 330 hnpInputStream.read(bytes, 0, 4); 331 if (!isElfFile(bytes)) { 332 hnpInputStream.closeEntry(); 333 continue; 334 } 335 // read input stream end to get entry size, can be adjusted based on performance testing 336 byte[] tmp = new byte[4096]; 337 int readLen; 338 do { 339 readLen = hnpInputStream.read(tmp, 0, 4096); 340 } while (readLen > 0); 341 elfEntries.put(libEntry.getName(), libEntry.getSize()); 342 hnpInputStream.closeEntry(); 343 } 344 } 345 return elfEntries; 346 } 347 348 /** 349 * Get entry name of all native files in hap 350 * 351 * @param hap the given hap 352 * @return list of entry name 353 */ getNativeEntriesFromHap(JarFile hap)354 private List<String> getNativeEntriesFromHap(JarFile hap) { 355 List<String> result = new ArrayList<>(); 356 for (Enumeration<JarEntry> e = hap.entries(); e.hasMoreElements();) { 357 JarEntry entry = e.nextElement(); 358 if (!entry.isDirectory()) { 359 if (!isNativeFile(entry.getName())) { 360 continue; 361 } 362 result.add(entry.getName()); 363 } 364 } 365 return result; 366 } 367 368 /** 369 * Check whether the entry is a native file 370 * 371 * @param entryName the name of entry 372 * @return true if it is a native file, and false otherwise 373 */ isNativeFile(String entryName)374 private boolean isNativeFile(String entryName) { 375 if (StringUtils.isEmpty(entryName)) { 376 return false; 377 } 378 if (entryName.endsWith(NATIVE_LIB_AN_SUFFIX)) { 379 return true; 380 } 381 if (entryName.startsWith(FileUtils.LIBS_PATH_PREFIX)) { 382 return true; 383 } 384 return false; 385 } 386 isElfFile(byte[] bytes)387 private boolean isElfFile(byte[] bytes) { 388 if (bytes == null || bytes.length != 4) { 389 return false; 390 } 391 return bytes[0] == 0x7F && bytes[1] == 0x45 && bytes[2] == 0x4C && bytes[3] == 0x46; 392 } 393 394 /** 395 * Sign specific entries in a hap 396 * 397 * @param entryNames list of entries which need to be signed 398 * @param hap input hap 399 * @param ownerID app-id in signature to identify 400 * @return sign info and merkle tree of each file 401 * @throws CodeSignException sign error 402 */ signFilesFromJar(List<String> entryNames, JarFile hap, String ownerID)403 private List<Pair<String, SignInfo>> signFilesFromJar(List<String> entryNames, JarFile hap, String ownerID) 404 throws CodeSignException { 405 List<Pair<String, SignInfo>> nativeLibInfoList = entryNames.stream().parallel().map(name -> { 406 LOGGER.debug("Sign entry name = {}", name); 407 JarEntry inEntry = hap.getJarEntry(name); 408 try (InputStream inputStream = hap.getInputStream(inEntry)) { 409 long fileSize = inEntry.getSize(); 410 // We don't store merkle tree in code signing of native libs 411 // Therefore, the second value of pair returned is ignored 412 Pair<SignInfo, byte[]> pairSignInfoAndMerkleTreeBytes = signFile(inputStream, fileSize, false, 0, 413 ownerID); 414 return Pair.create(name, pairSignInfoAndMerkleTreeBytes.getFirst()); 415 } catch (FsVerityDigestException | CodeSignException | IOException e) { 416 LOGGER.error("Sign lib error, entry name = {}, msg : {}", name, e.getMessage()); 417 } 418 return null; 419 }).collect(Collectors.toList()); 420 if (nativeLibInfoList.contains(null)) { 421 throw new CodeSignException("Sign lib error"); 422 } 423 return nativeLibInfoList; 424 } 425 426 /** 427 * Sign a file from input stream 428 * 429 * @param inputStream input stream of a file 430 * @param fileSize size of the file 431 * @param storeTree whether to store merkle tree in signed info 432 * @param fsvTreeOffset merkle tree raw bytes offset based on the start of file 433 * @param ownerID app-id in signature to identify 434 * @return pair of signature and tree 435 * @throws FsVerityDigestException computing FsVerity Digest error 436 * @throws CodeSignException signing error 437 */ signFile(InputStream inputStream, long fileSize, boolean storeTree, long fsvTreeOffset, String ownerID)438 public Pair<SignInfo, byte[]> signFile(InputStream inputStream, long fileSize, boolean storeTree, 439 long fsvTreeOffset, String ownerID) throws FsVerityDigestException, CodeSignException { 440 FsVerityGenerator fsVerityGenerator = new FsVerityGenerator(); 441 fsVerityGenerator.generateFsVerityDigest(inputStream, fileSize, fsvTreeOffset); 442 byte[] fsVerityDigest = fsVerityGenerator.getFsVerityDigest(); 443 byte[] signature = generateSignature(fsVerityDigest, ownerID); 444 int flags = 0; 445 if (storeTree) { 446 flags = SignInfo.FLAG_MERKLE_TREE_INCLUDED; 447 } 448 SignInfo signInfo = new SignInfo(fsVerityGenerator.getSaltSize(), flags, fileSize, fsVerityGenerator.getSalt(), 449 signature); 450 // if store merkle tree in sign info 451 if (storeTree) { 452 int merkleTreeSize = fsVerityGenerator.getTreeBytes() == null ? 0 : fsVerityGenerator.getTreeBytes().length; 453 Extension merkleTreeExtension = new MerkleTreeExtension(merkleTreeSize, fsvTreeOffset, 454 fsVerityGenerator.getRootHash()); 455 signInfo.addExtension(merkleTreeExtension); 456 } 457 return Pair.create(signInfo, fsVerityGenerator.getTreeBytes()); 458 } 459 generateSignature(byte[] signedData, String ownerID)460 private byte[] generateSignature(byte[] signedData, String ownerID) throws CodeSignException { 461 SignerConfig copiedConfig = signConfig; 462 // signConfig is created by SignerFactory 463 if ((copiedConfig.getSigner() instanceof LocalSigner)) { 464 if (copiedConfig.getCertificates().isEmpty()) { 465 throw new CodeSignException("No certificates configured for sign"); 466 } 467 BcSignedDataGenerator bcSignedDataGenerator = new BcSignedDataGenerator(); 468 bcSignedDataGenerator.setOwnerID(ownerID); 469 return bcSignedDataGenerator.generateSignedData(signedData, copiedConfig); 470 } else { 471 copiedConfig = signConfig.copy(); 472 BcSignedDataGenerator bcSignedDataGenerator = new BcSignedDataGenerator(); 473 bcSignedDataGenerator.setOwnerID(ownerID); 474 return bcSignedDataGenerator.generateSignedData(signedData, copiedConfig); 475 } 476 } 477 478 /** 479 * At here, segment header, fsverity info/hap/so info segment, merkle tree 480 * segment should all be generated. 481 * code sign block size, segment number, offset is not updated. 482 * Try to update whatever could be updated here. 483 * 484 * @param codeSignBlock CodeSignBlock 485 */ updateCodeSignBlock(CodeSignBlock codeSignBlock)486 private void updateCodeSignBlock(CodeSignBlock codeSignBlock) { 487 // construct segment header list 488 codeSignBlock.setSegmentHeaders(); 489 // Compute and set segment number 490 codeSignBlock.setSegmentNum(); 491 // update code sign block header flag 492 codeSignBlock.setCodeSignBlockFlag(); 493 // compute segment offset 494 codeSignBlock.computeSegmentOffset(); 495 } 496 497 } 498