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.hap.config.SignerConfig; 31 import com.ohos.hapsigntool.entity.Pair; 32 import com.ohos.hapsigntool.error.HapFormatException; 33 import com.ohos.hapsigntool.error.ProfileException; 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.UnsignedDecimalUtil; 38 import com.ohos.hapsigntool.zip.Zip; 39 import com.ohos.hapsigntool.zip.ZipEntryHeader; 40 import com.ohos.hapsigntool.zip.ZipEntry; 41 42 import org.apache.logging.log4j.LogManager; 43 import org.apache.logging.log4j.Logger; 44 45 import java.io.File; 46 import java.io.FileInputStream; 47 import java.io.IOException; 48 import java.io.InputStream; 49 import java.nio.ByteBuffer; 50 import java.nio.ByteOrder; 51 import java.util.ArrayList; 52 import java.util.Enumeration; 53 import java.util.List; 54 import java.util.Locale; 55 import java.util.jar.JarEntry; 56 import java.util.jar.JarFile; 57 import java.util.regex.Matcher; 58 import java.util.regex.Pattern; 59 60 /** 61 * core functions of code signing 62 * 63 * @since 2023/06/05 64 */ 65 public class CodeSigning { 66 /** 67 * Only hap and hsp bundle supports code signing 68 */ 69 public static final String[] SUPPORT_FILE_FORM = {"hap", "hsp"}; 70 71 /** 72 * Only elf file supports bin code signing 73 */ 74 public static final String SUPPORT_BIN_FILE_FORM = "elf"; 75 76 /** 77 * Defined entry name of hap file 78 */ 79 public static final String HAP_SIGNATURE_ENTRY_NAME = "Hap"; 80 81 private static final Logger LOGGER = LogManager.getLogger(CodeSigning.class); 82 83 private static final String NATIVE_LIB_AN_SUFFIX = ".an"; 84 85 private static final String NATIVE_LIB_SO_SUFFIX = ".so"; 86 87 private final List<String> extractedNativeLibSuffixs = new ArrayList<>(); 88 89 private final SignerConfig signConfig; 90 91 private CodeSignBlock codeSignBlock; 92 93 private long timestamp = 0L; 94 95 /** 96 * provide code sign functions to sign a hap 97 * 98 * @param signConfig configuration of sign 99 */ CodeSigning(SignerConfig signConfig)100 public CodeSigning(SignerConfig signConfig) { 101 this.signConfig = signConfig; 102 } 103 104 /** 105 * Sign the given elf file, and pack all signature into output file 106 * 107 * @param input file to sign 108 * @param offset position of codesign block based on start of the file 109 * @param inForm file's format 110 * @param profileContent profile of the elf 111 * @return byte array of code sign block 112 * @throws CodeSignException code signing exception 113 * @throws IOException io error 114 * @throws FsVerityDigestException computing FsVerity digest error 115 * @throws ProfileException profile of elf is invalid 116 */ getElfCodeSignBlock(File input, long offset, String inForm, String profileContent)117 public byte[] getElfCodeSignBlock(File input, long offset, String inForm, String profileContent) 118 throws CodeSignException, FsVerityDigestException, IOException, ProfileException { 119 if (!SUPPORT_BIN_FILE_FORM.equalsIgnoreCase(inForm)) { 120 throw new CodeSignException("file's format is unsupported"); 121 } 122 long fileSize = input.length(); 123 int paddingSize = ElfSignBlock.computeMerkleTreePaddingLength(offset); 124 long fsvTreeOffset = offset + Integer.BYTES * 2 + paddingSize; 125 try (FileInputStream inputStream = new FileInputStream(input)) { 126 FsVerityGenerator fsVerityGenerator = new FsVerityGenerator(); 127 fsVerityGenerator.generateFsVerityDigest(inputStream, fileSize, fsvTreeOffset); 128 byte[] fsVerityDigest = fsVerityGenerator.getFsVerityDigest(); 129 // ownerID should be DEBUG_LIB_ID while signing ELF 130 String ownerID = (profileContent == null) ? "DEBUG_LIB_ID" : HapUtils.getAppIdentifier(profileContent); 131 byte[] signature = generateSignature(fsVerityDigest, ownerID); 132 // add fs-verify info 133 FsVerityDescriptor.Builder fsdbuilder = new FsVerityDescriptor.Builder().setFileSize(fileSize) 134 .setHashAlgorithm(FsVerityGenerator.getFsVerityHashAlgorithm()) 135 .setLog2BlockSize(FsVerityGenerator.getLog2BlockSize()) 136 .setSaltSize((byte) fsVerityGenerator.getSaltSize()) 137 .setSignSize(signature.length) 138 .setFileSize(fileSize) 139 .setSalt(fsVerityGenerator.getSalt()) 140 .setRawRootHash(fsVerityGenerator.getRootHash()) 141 .setFlags(FsVerityDescriptor.FLAG_STORE_MERKLE_TREE_OFFSET) 142 .setMerkleTreeOffset(fsvTreeOffset) 143 .setCsVersion(FsVerityDescriptor.CODE_SIGN_VERSION); 144 FsVerityDescriptorWithSign fsVerityDescriptorWithSign = new FsVerityDescriptorWithSign(fsdbuilder.build(), 145 signature); 146 byte[] treeBytes = fsVerityGenerator.getTreeBytes(); 147 ElfSignBlock signBlock = new ElfSignBlock(paddingSize, treeBytes, fsVerityDescriptorWithSign); 148 LOGGER.info("Sign elf successfully."); 149 return signBlock.toByteArray(); 150 } 151 } 152 153 /** 154 * Sign the given hap file, and pack all signature into output file 155 * 156 * @param input file to sign 157 * @param offset position of codesign block based on start of the file 158 * @param inForm file's format 159 * @param profileContent profile of the hap 160 * @param zip zip 161 * @return byte array of code sign block 162 * @throws CodeSignException code signing exception 163 * @throws IOException io error 164 * @throws HapFormatException hap format invalid 165 * @throws FsVerityDigestException computing FsVerity digest error 166 * @throws ProfileException profile of the hap error 167 */ getCodeSignBlock(File input, long offset, String inForm, String profileContent, Zip zip)168 public byte[] getCodeSignBlock(File input, long offset, String inForm, String profileContent, Zip zip) 169 throws CodeSignException, IOException, HapFormatException, FsVerityDigestException, ProfileException { 170 LOGGER.info("Start to sign code."); 171 if (!StringUtils.containsIgnoreCase(SUPPORT_FILE_FORM, inForm)) { 172 throw new CodeSignException("file's format is unsupported"); 173 } 174 long dataSize = computeDataSize(zip); 175 timestamp = System.currentTimeMillis(); 176 // generate CodeSignBlock 177 this.codeSignBlock = new CodeSignBlock(); 178 // compute merkle tree offset, replace with computeMerkleTreeOffset if fs-verity descriptor supports 179 long fsvTreeOffset = this.codeSignBlock.computeMerkleTreeOffset(offset); 180 // update fs-verity segment 181 FsVerityInfoSegment fsVerityInfoSegment = new FsVerityInfoSegment(FsVerityDescriptor.VERSION, 182 FsVerityGenerator.getFsVerityHashAlgorithm(), FsVerityGenerator.getLog2BlockSize()); 183 this.codeSignBlock.setFsVerityInfoSegment(fsVerityInfoSegment); 184 185 LOGGER.debug("Sign hap."); 186 String ownerID = HapUtils.getAppIdentifier(profileContent); 187 188 try (FileInputStream inputStream = new FileInputStream(input)) { 189 Pair<SignInfo, byte[]> hapSignInfoAndMerkleTreeBytesPair = signFile(inputStream, dataSize, true, 190 fsvTreeOffset, ownerID); 191 // update hap segment in CodeSignBlock 192 this.codeSignBlock.getHapInfoSegment().setSignInfo(hapSignInfoAndMerkleTreeBytesPair.getFirst()); 193 // Insert merkle tree bytes into code sign block 194 this.codeSignBlock.addOneMerkleTree(HAP_SIGNATURE_ENTRY_NAME, 195 hapSignInfoAndMerkleTreeBytesPair.getSecond()); 196 } 197 // update native lib info segment in CodeSignBlock 198 signNativeLibs(input, ownerID); 199 200 // last update codeSignBlock before generating its byte array representation 201 updateCodeSignBlock(this.codeSignBlock); 202 203 // complete code sign block byte array here 204 byte[] generated = this.codeSignBlock.generateCodeSignBlockByte(fsvTreeOffset); 205 LOGGER.info("Sign successfully."); 206 return generated; 207 } 208 computeDataSize(Zip zip)209 private long computeDataSize(Zip zip) throws HapFormatException { 210 long dataSize = 0L; 211 for (ZipEntry entry : zip.getZipEntries()) { 212 ZipEntryHeader zipEntryHeader = entry.getZipEntryData().getZipEntryHeader(); 213 if (FileUtils.isRunnableFile(zipEntryHeader.getFileName()) 214 && zipEntryHeader.getMethod() == Zip.FILE_UNCOMPRESS_METHOD_FLAG) { 215 continue; 216 } 217 // if the first file is not uncompressed abc or so, set dataSize to zero 218 if (entry.getCentralDirectory().getOffset() == 0) { 219 break; 220 } 221 // the first entry which is not abc/so/an is found, return its data offset 222 dataSize = entry.getCentralDirectory().getOffset() + ZipEntryHeader.HEADER_LENGTH 223 + zipEntryHeader.getFileNameLength() + zipEntryHeader.getExtraLength(); 224 break; 225 } 226 if ((dataSize % CodeSignBlock.PAGE_SIZE_4K) != 0) { 227 throw new HapFormatException( 228 String.format(Locale.ROOT, "Invalid dataSize(%d), not a multiple of 4096", dataSize)); 229 } 230 return dataSize; 231 } 232 signNativeLibs(File input, String ownerID)233 private void signNativeLibs(File input, String ownerID) throws IOException, FsVerityDigestException, 234 CodeSignException { 235 // 'an' libs are always signed 236 extractedNativeLibSuffixs.add(NATIVE_LIB_AN_SUFFIX); 237 // 'so' libs are always signed 238 extractedNativeLibSuffixs.add(NATIVE_LIB_SO_SUFFIX); 239 240 // sign native files 241 try (JarFile inputJar = new JarFile(input, false)) { 242 List<String> entryNames = getNativeEntriesFromHap(inputJar); 243 if (entryNames.isEmpty()) { 244 LOGGER.info("No native libs."); 245 return; 246 } 247 List<Pair<String, SignInfo>> nativeLibInfoList = signFilesFromJar(entryNames, inputJar, ownerID); 248 // update SoInfoSegment in CodeSignBlock 249 this.codeSignBlock.getSoInfoSegment().setSoInfoList(nativeLibInfoList); 250 } 251 } 252 253 /** 254 * Get entry name of all native files in hap 255 * 256 * @param hap the given hap 257 * @return list of entry name 258 */ getNativeEntriesFromHap(JarFile hap)259 private List<String> getNativeEntriesFromHap(JarFile hap) { 260 List<String> result = new ArrayList<>(); 261 for (Enumeration<JarEntry> e = hap.entries(); e.hasMoreElements();) { 262 JarEntry entry = e.nextElement(); 263 if (!entry.isDirectory()) { 264 if (!isNativeFile(entry.getName())) { 265 continue; 266 } 267 result.add(entry.getName()); 268 } 269 } 270 return result; 271 } 272 273 /** 274 * Check whether the entry is a native file 275 * 276 * @param entryName the name of entry 277 * @return true if it is a native file, and false otherwise 278 */ isNativeFile(String entryName)279 private boolean isNativeFile(String entryName) { 280 if (StringUtils.isEmpty(entryName)) { 281 return false; 282 } 283 if (entryName.endsWith(NATIVE_LIB_AN_SUFFIX)) { 284 return true; 285 } 286 if (extractedNativeLibSuffixs.contains(NATIVE_LIB_SO_SUFFIX)) { 287 Pattern pattern = FileUtils.SUFFIX_REGEX_MAP.get("so"); 288 Matcher matcher = pattern.matcher(entryName); 289 if (matcher.find()) { 290 return true; 291 } 292 } 293 return false; 294 } 295 296 /** 297 * Sign specific entries in a hap 298 * 299 * @param entryNames list of entries which need to be signed 300 * @param hap input hap 301 * @param ownerID app-id in signature to identify 302 * @return sign info and merkle tree of each file 303 * @throws IOException io error 304 * @throws FsVerityDigestException computing FsVerity digest error 305 * @throws CodeSignException sign error 306 */ signFilesFromJar(List<String> entryNames, JarFile hap, String ownerID)307 private List<Pair<String, SignInfo>> signFilesFromJar(List<String> entryNames, JarFile hap, String ownerID) 308 throws IOException, FsVerityDigestException, CodeSignException { 309 List<Pair<String, SignInfo>> nativeLibInfoList = new ArrayList<>(); 310 for (String name : entryNames) { 311 LOGGER.debug("Sign entry name = " + name); 312 JarEntry inEntry = hap.getJarEntry(name); 313 try (InputStream inputStream = hap.getInputStream(inEntry)) { 314 long fileSize = inEntry.getSize(); 315 // We don't store merkle tree in code signing of native libs 316 // Therefore, the second value of pair returned is ignored 317 Pair<SignInfo, byte[]> pairSignInfoAndMerkleTreeBytes = signFile(inputStream, fileSize, 318 false, 0, ownerID); 319 nativeLibInfoList.add(Pair.create(name, pairSignInfoAndMerkleTreeBytes.getFirst())); 320 } 321 } 322 return nativeLibInfoList; 323 } 324 325 /** 326 * Sign a file from input stream 327 * 328 * @param inputStream input stream of a file 329 * @param fileSize size of the file 330 * @param storeTree whether to store merkle tree in signed info 331 * @param fsvTreeOffset merkle tree raw bytes offset based on the start of file 332 * @param ownerID app-id in signature to identify 333 * @return pair of signature and tree 334 * @throws FsVerityDigestException computing FsVerity Digest error 335 * @throws CodeSignException signing error 336 */ signFile(InputStream inputStream, long fileSize, boolean storeTree, long fsvTreeOffset, String ownerID)337 public Pair<SignInfo, byte[]> signFile(InputStream inputStream, long fileSize, boolean storeTree, 338 long fsvTreeOffset, String ownerID) throws FsVerityDigestException, CodeSignException { 339 FsVerityGenerator fsVerityGenerator = new FsVerityGenerator(); 340 fsVerityGenerator.generateFsVerityDigest(inputStream, fileSize, fsvTreeOffset); 341 byte[] fsVerityDigest = fsVerityGenerator.getFsVerityDigest(); 342 byte[] signature = generateSignature(fsVerityDigest, ownerID); 343 int flags = 0; 344 if (storeTree) { 345 flags = SignInfo.FLAG_MERKLE_TREE_INCLUDED; 346 } 347 SignInfo signInfo = new SignInfo(fsVerityGenerator.getSaltSize(), flags, fileSize, fsVerityGenerator.getSalt(), 348 signature); 349 // if store merkle tree in sign info 350 if (storeTree) { 351 int merkleTreeSize = fsVerityGenerator.getTreeBytes() == null ? 0 : fsVerityGenerator.getTreeBytes().length; 352 Extension merkleTreeExtension = new MerkleTreeExtension(merkleTreeSize, fsvTreeOffset, 353 fsVerityGenerator.getRootHash()); 354 signInfo.addExtension(merkleTreeExtension); 355 } 356 return Pair.create(signInfo, fsVerityGenerator.getTreeBytes()); 357 } 358 generateSignature(byte[] signedData, String ownerID)359 private byte[] generateSignature(byte[] signedData, String ownerID) throws CodeSignException { 360 // signConfig is created by SignerFactory 361 if ((signConfig.getSigner() instanceof LocalSigner)) { 362 if (signConfig.getCertificates().isEmpty()) { 363 throw new CodeSignException("No certificates configured for sign"); 364 } 365 } 366 367 BcSignedDataGenerator bcSignedDataGenerator = new BcSignedDataGenerator(); 368 bcSignedDataGenerator.setOwnerID(ownerID); 369 return bcSignedDataGenerator.generateSignedData(signedData, signConfig); 370 } 371 372 /** 373 * At here, segment header, fsverity info/hap/so info segment, merkle tree 374 * segment should all be generated. 375 * code sign block size, segment number, offset is not updated. 376 * Try to update whatever could be updated here. 377 * 378 * @param codeSignBlock CodeSignBlock 379 */ updateCodeSignBlock(CodeSignBlock codeSignBlock)380 private void updateCodeSignBlock(CodeSignBlock codeSignBlock) { 381 // construct segment header list 382 codeSignBlock.setSegmentHeaders(); 383 // Compute and set segment number 384 codeSignBlock.setSegmentNum(); 385 // update code sign block header flag 386 codeSignBlock.setCodeSignBlockFlag(); 387 // compute segment offset 388 codeSignBlock.computeSegmentOffset(); 389 } 390 parseCentralDirectory(byte[] buffer, int count)391 private List<CentralDirectory> parseCentralDirectory(byte[] buffer, int count) { 392 List<CentralDirectory> cdList = new ArrayList<>(); 393 ByteBuffer cdBuffer = ByteBuffer.allocate(buffer.length).order(ByteOrder.LITTLE_ENDIAN); 394 cdBuffer.put(buffer); 395 cdBuffer.rewind(); 396 for (int i = 0; i < count; i++) { 397 byte[] bytesBeforeCompressionMethod = new byte[CentralDirectory.BYTE_SIZE_BEFORE_COMPRESSION_METHOD]; 398 cdBuffer.get(bytesBeforeCompressionMethod); 399 char compressionMode = cdBuffer.getChar(); 400 CentralDirectory.Builder builder = new CentralDirectory.Builder().setCompressionMethod(compressionMode); 401 byte[] bytesBetweenCmprMethodAndFileNameLength = 402 new byte[CentralDirectory.BYTE_SIZE_BETWEEN_COMPRESSION_MODE_AND_FILE_SIZE]; 403 cdBuffer.get(bytesBetweenCmprMethodAndFileNameLength); 404 char fileNameLength = cdBuffer.getChar(); 405 char extraFieldLength = cdBuffer.getChar(); 406 char fileCommentLength = cdBuffer.getChar(); 407 byte[] attributes = 408 new byte[CentralDirectory.BYTE_SIZE_BETWEEN_FILE_COMMENT_LENGTH_AND_LOCHDR_RELATIVE_OFFSET]; 409 cdBuffer.get(attributes); 410 long locHdrOffset = UnsignedDecimalUtil.getUnsignedInt(cdBuffer); 411 builder.setFileNameLength(fileNameLength).setExtraFieldLength(extraFieldLength) 412 .setFileCommentLength(fileCommentLength).setRelativeOffsetOfLocalHeader(locHdrOffset); 413 byte[] fileNameBuffer = new byte[fileNameLength]; 414 cdBuffer.get(fileNameBuffer); 415 if (extraFieldLength != 0) { 416 cdBuffer.get(new byte[extraFieldLength]); 417 } 418 if (fileCommentLength != 0) { 419 cdBuffer.get(new byte[fileCommentLength]); 420 } 421 CentralDirectory cd = builder.setFileName(fileNameBuffer).build(); 422 cdList.add(cd); 423 } 424 425 return cdList; 426 } 427 } 428