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.CodeSignBlockHeader; 20 import com.ohos.hapsigntool.codesigning.datastructure.ElfSignBlock; 21 import com.ohos.hapsigntool.codesigning.datastructure.Extension; 22 import com.ohos.hapsigntool.codesigning.datastructure.FsVerityInfoSegment; 23 import com.ohos.hapsigntool.codesigning.datastructure.HapInfoSegment; 24 import com.ohos.hapsigntool.codesigning.datastructure.MerkleTreeExtension; 25 import com.ohos.hapsigntool.codesigning.datastructure.NativeLibInfoSegment; 26 import com.ohos.hapsigntool.codesigning.datastructure.SegmentHeader; 27 import com.ohos.hapsigntool.codesigning.datastructure.SignInfo; 28 import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException; 29 import com.ohos.hapsigntool.codesigning.exception.PageInfoException; 30 import com.ohos.hapsigntool.codesigning.exception.VerifyCodeSignException; 31 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityGenerator; 32 import com.ohos.hapsigntool.codesigning.utils.CmsUtils; 33 import com.ohos.hapsigntool.codesigning.utils.HapUtils; 34 import com.ohos.hapsigntool.entity.Pair; 35 import com.ohos.hapsigntool.error.ProfileException; 36 import com.ohos.hapsigntool.utils.LogUtils; 37 import com.ohos.hapsigntool.utils.StringUtils; 38 39 import org.bouncycastle.asn1.ASN1ObjectIdentifier; 40 import org.bouncycastle.asn1.cms.Attribute; 41 import org.bouncycastle.asn1.cms.AttributeTable; 42 import org.bouncycastle.cms.CMSException; 43 import org.bouncycastle.cms.CMSSignedData; 44 import org.bouncycastle.cms.SignerInformation; 45 46 import java.io.File; 47 import java.io.FileInputStream; 48 import java.io.IOException; 49 import java.io.InputStream; 50 import java.util.Arrays; 51 import java.util.Collection; 52 import java.util.HashMap; 53 import java.util.HashSet; 54 import java.util.Locale; 55 import java.util.Map; 56 import java.util.Set; 57 import java.util.jar.JarEntry; 58 import java.util.jar.JarFile; 59 import java.util.zip.ZipInputStream; 60 61 /** 62 * Verify code signature given a file with code sign block 63 * 64 * @since 2023/09/08 65 */ 66 public class VerifyCodeSignature { 67 private static final LogUtils LOGGER = new LogUtils(VerifyCodeSignature.class); 68 checkOwnerID(byte[] signature, String profileOwnerID, String profileType)69 private static void checkOwnerID(byte[] signature, String profileOwnerID, String profileType) 70 throws CMSException, VerifyCodeSignException { 71 String ownerID = profileOwnerID; 72 // if profileType is debug, check the app-id in signature, should be null or DEBUG_LIB_ID 73 if ("debug".equals(profileType)) { 74 ownerID = HapUtils.HAP_DEBUG_OWNER_ID; 75 } 76 checkSignatureOwnerID(ownerID, signature, profileType); 77 } 78 checkHnpOwnerID(byte[] signature, String profileOwnerID, String profileType, String hnpType)79 private static void checkHnpOwnerID(byte[] signature, String profileOwnerID, String profileType, String hnpType) 80 throws CMSException, VerifyCodeSignException { 81 String ownerID = profileOwnerID; 82 // if profileType is debug, check the app-id in signature, should be null or DEBUG_LIB_ID 83 if ("debug".equals(profileType)) { 84 ownerID = HapUtils.HAP_DEBUG_OWNER_ID; 85 } else if ("release".equals(profileType)) { 86 if ("public".equals(hnpType)) { 87 ownerID = HapUtils.HAP_SHARED_OWNER_ID; 88 } 89 } 90 checkSignatureOwnerID(ownerID, signature, profileType); 91 } 92 checkSignatureOwnerID(String ownerID, byte[] signature, String profileType)93 private static void checkSignatureOwnerID(String ownerID, byte[] signature, String profileType) 94 throws CMSException, VerifyCodeSignException { 95 CMSSignedData cmsSignedData = new CMSSignedData(signature); 96 Collection<SignerInformation> signers = cmsSignedData.getSignerInfos().getSigners(); 97 for (SignerInformation signer : signers) { 98 AttributeTable attrTable = signer.getSignedAttributes(); 99 Attribute attr = attrTable.get(new ASN1ObjectIdentifier(BcSignedDataGenerator.SIGNER_OID)); 100 // if app-id is null, if profileType is debug, it's ok. 101 if (attr == null) { 102 if ("debug".equals(profileType)) { 103 continue; 104 } 105 if (ownerID == null) { 106 continue; 107 } else { 108 throw new VerifyCodeSignException("app-identifier is not in the signature"); 109 } 110 } 111 if (ownerID == null) { 112 throw new VerifyCodeSignException("app-identifier in profile is null, but is not null in signature"); 113 } 114 // if app-id in signature exists, it should be equal to the app-id in profile. 115 String resultOwnerID = attr.getAttrValues().getObjectAt(0).toString(); 116 if (!ownerID.equals(resultOwnerID)) { 117 throw new VerifyCodeSignException("app-identifier in signature is invalid"); 118 } 119 } 120 } 121 122 /** 123 * Verify a signed elf's signature 124 * 125 * @param file signed elf file 126 * @param offset start position of code sign block based on the start of the elf file 127 * @param length byte size of code sign block 128 * @param fileFormat elf 129 * @param profileContent profile json string 130 * @return true if signature verify succeed and false otherwise 131 * @throws IOException If an input or output exception occurred 132 * @throws VerifyCodeSignException parsing result invalid 133 * @throws FsVerityDigestException if fs-verity digest generation failed 134 * @throws CMSException if signature verify failed 135 * @throws ProfileException if verify profile failed 136 */ verifyElf(File file, long offset, long length, String fileFormat, String profileContent)137 public static boolean verifyElf(File file, long offset, long length, String fileFormat, String profileContent) 138 throws IOException, VerifyCodeSignException, FsVerityDigestException, CMSException, ProfileException { 139 if (!CodeSigning.SUPPORT_BIN_FILE_FORM.equalsIgnoreCase(fileFormat)) { 140 LOGGER.info("Not elf file, skip code signing verify"); 141 return true; 142 } 143 // 1) parse sign block to ElfCodeSignBlock object 144 ElfSignBlock elfSignBlock; 145 try (FileInputStream signedElf = new FileInputStream(file)) { 146 byte[] codeSignBlockBytes = new byte[(int) length]; 147 signedElf.skip(offset); 148 signedElf.read(codeSignBlockBytes); 149 elfSignBlock = ElfSignBlock.fromByteArray(codeSignBlockBytes); 150 } 151 // 2) verify file data 152 try (FileInputStream signedElf = new FileInputStream(file)) { 153 int paddingSize = ElfSignBlock.computeMerkleTreePaddingLength(offset); 154 byte[] merkleTreeWithPadding = elfSignBlock.getMerkleTreeWithPadding(); 155 byte[] merkleTree = Arrays.copyOfRange(merkleTreeWithPadding, paddingSize, merkleTreeWithPadding.length); 156 verifySingleFile(signedElf, elfSignBlock.getDataSize(), elfSignBlock.getSignature(), 157 elfSignBlock.getTreeOffset(), merkleTree); 158 } 159 if (profileContent != null) { 160 Pair<String, String> pairResult = HapUtils.parseAppIdentifier(profileContent); 161 checkOwnerID(elfSignBlock.getSignature(), pairResult.getFirst(), pairResult.getSecond()); 162 } 163 return true; 164 } 165 166 /** 167 * Verify a signed hap's signature 168 * 169 * @param file signed hap file 170 * @param offset start position of code sign block based on the start of the hap file 171 * @param length byte size of code sign block 172 * @param fileFormat hap or hqf or hsp, etc. 173 * @param profileContent profile of the hap 174 * @return true if signature verify succeed and false otherwise 175 * @throws IOException If an input or output exception occurred 176 * @throws VerifyCodeSignException parsing result invalid 177 * @throws FsVerityDigestException if fs-verity digest generation failed 178 * @throws CMSException if signature verify failed 179 * @throws ProfileException profile of the hap failed 180 */ verifyHap(File file, long offset, long length, String fileFormat, String profileContent)181 public static boolean verifyHap(File file, long offset, long length, String fileFormat, String profileContent) 182 throws IOException, VerifyCodeSignException, FsVerityDigestException, CMSException, ProfileException { 183 if (!StringUtils.containsIgnoreCase(CodeSigning.SUPPORT_FILE_FORM, fileFormat)) { 184 LOGGER.info("Not hap, hsp or hqf file, skip code signing verify"); 185 return true; 186 } 187 Pair<String, String> pairResult = HapUtils.parseAppIdentifier(profileContent); 188 189 CodeSignBlock csb = generateCodeSignBlock(file, offset, length); 190 // 2) verify hap 191 try (FileInputStream hap = new FileInputStream(file)) { 192 long dataSize = csb.getHapInfoSegment().getSignInfo().getDataSize(); 193 byte[] signature = csb.getHapInfoSegment().getSignInfo().getSignature(); 194 Extension extension = csb.getHapInfoSegment() 195 .getSignInfo() 196 .getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED); 197 MerkleTreeExtension mte = new MerkleTreeExtension(0, 0, null); 198 if (extension instanceof MerkleTreeExtension) { 199 mte = (MerkleTreeExtension) extension; 200 } 201 // temporary: merkle tree offset set to zero, change to merkleTreeOffset 202 verifySingleFile(hap, dataSize, signature, mte.getMerkleTreeOffset(), 203 csb.getOneMerkleTreeByFileName(CodeSigning.HAP_SIGNATURE_ENTRY_NAME)); 204 checkOwnerID(signature, pairResult.getFirst(), pairResult.getSecond()); 205 } 206 // 3) verify native libs 207 verifyLibs(file, csb, pairResult); 208 return true; 209 } 210 verifyLibs(File file, CodeSignBlock csb, Pair<String, String> pairResult)211 private static void verifyLibs(File file, CodeSignBlock csb, Pair<String, String> pairResult) 212 throws IOException, FsVerityDigestException, VerifyCodeSignException, CMSException, ProfileException { 213 try (JarFile inputJar = new JarFile(file, false)) { 214 Map<String, SignInfo> hnpLibSignInfoMap = new HashMap<>(); 215 Set<String> hnpEntryNames = new HashSet<>(); 216 for (int i = 0; i < csb.getSoInfoSegment().getSectionNum(); i++) { 217 String entryName = csb.getSoInfoSegment().getFileNameList().get(i); 218 SignInfo signInfo = csb.getSoInfoSegment().getSignInfoList().get(i); 219 if (entryName.contains("!/")) { 220 String[] filePath = entryName.split("!/"); 221 hnpEntryNames.add(filePath[0]); 222 hnpLibSignInfoMap.put(entryName, signInfo); 223 } else { 224 LOGGER.info("verify lib: {}", entryName); 225 verifyHapLib(inputJar, entryName, signInfo, pairResult); 226 } 227 } 228 if (hnpEntryNames.isEmpty()) { 229 // not exists hnp lib 230 return; 231 } 232 // get module.json 233 Map<String, String> hnpTypeMap = HapUtils.getHnpsFromJson(inputJar); 234 for (String hnpEntryName : hnpEntryNames) { 235 verifyHnpLib(inputJar, hnpEntryName, hnpLibSignInfoMap, hnpTypeMap, pairResult); 236 } 237 } 238 } 239 verifyHapLib(JarFile inputJar, String entryName, SignInfo signInfo, Pair<String, String> pairResult)240 private static void verifyHapLib(JarFile inputJar, String entryName, SignInfo signInfo, 241 Pair<String, String> pairResult) 242 throws IOException, FsVerityDigestException, VerifyCodeSignException, CMSException { 243 244 JarEntry entry = inputJar.getJarEntry(entryName); 245 if (entry.getSize() != signInfo.getDataSize()) { 246 throw new VerifyCodeSignException( 247 String.format(Locale.ROOT, "Invalid dataSize of native lib %s", entryName)); 248 } 249 byte[] entrySig = signInfo.getSignature(); 250 try (InputStream entryInputStream = inputJar.getInputStream(entry)) { 251 // temporary merkleTreeOffset 0 252 verifySingleFile(entryInputStream, entry.getSize(), entrySig, 0, null); 253 checkOwnerID(entrySig, pairResult.getFirst(), pairResult.getSecond()); 254 } 255 } 256 verifyHnpLib(JarFile inputJar, String hnpEntryName, Map<String, SignInfo> hnpLibSignInfoMap, Map<String, String> hnpTypeMap, Pair<String, String> pairResult)257 private static void verifyHnpLib(JarFile inputJar, String hnpEntryName, Map<String, SignInfo> hnpLibSignInfoMap, 258 Map<String, String> hnpTypeMap, Pair<String, String> pairResult) 259 throws IOException, FsVerityDigestException, VerifyCodeSignException, CMSException { 260 JarEntry hnpEntry = inputJar.getJarEntry(hnpEntryName); 261 try (InputStream inputStream = inputJar.getInputStream(hnpEntry); 262 ZipInputStream hnpInputStream = new ZipInputStream(inputStream)) { 263 String hnpFileName = HapUtils.parseHnpPath(hnpEntryName); 264 if (!hnpTypeMap.containsKey(hnpFileName)) { 265 throw new VerifyCodeSignException("hnp should be described in module.json"); 266 } 267 String hnpType = hnpTypeMap.get(hnpFileName); 268 java.util.zip.ZipEntry libEntry = null; 269 while ((libEntry = hnpInputStream.getNextEntry()) != null) { 270 String libPath = hnpEntry.getName() + "!/" + libEntry.getName(); 271 if (!hnpLibSignInfoMap.containsKey(libPath)) { 272 continue; 273 } 274 LOGGER.info("verify lib: {}", libPath); 275 SignInfo signInfo = hnpLibSignInfoMap.get(libPath); 276 byte[] entrySig = signInfo.getSignature(); 277 long dataSize = signInfo.getDataSize(); 278 verifySingleFile(hnpInputStream, dataSize, entrySig, 0, null); 279 checkHnpOwnerID(entrySig, pairResult.getFirst(), pairResult.getSecond(), hnpType); 280 hnpInputStream.closeEntry(); 281 } 282 } 283 } 284 generateCodeSignBlock(File file, long offset, long length)285 private static CodeSignBlock generateCodeSignBlock(File file, long offset, long length) 286 throws IOException, VerifyCodeSignException { 287 CodeSignBlock csb = new CodeSignBlock(); 288 // 1) parse sign block to CodeSignBlock object 289 try (FileInputStream signedHap = new FileInputStream(file)) { 290 int fileReadOffset = 0; 291 // 0) skip data part, but fileReadOffset remains at start(0) 292 signedHap.skip(offset); 293 // 1) parse codeSignBlockHeader 294 byte[] codeSignBlockHeaderByteArray = new byte[CodeSignBlockHeader.size()]; 295 fileReadOffset += signedHap.read(codeSignBlockHeaderByteArray); 296 csb.setCodeSignBlockHeader(CodeSignBlockHeader.fromByteArray(codeSignBlockHeaderByteArray)); 297 if (csb.getCodeSignBlockHeader().getBlockSize() != length) { 298 throw new VerifyCodeSignException("Invalid code Sign block size of setCodeSignBlockHeader"); 299 } 300 // 2) parse segment headers 301 for (int i = 0; i < csb.getCodeSignBlockHeader().getSegmentNum(); i++) { 302 byte[] segmentHeaderByteArray = new byte[SegmentHeader.SEGMENT_HEADER_LENGTH]; 303 fileReadOffset += signedHap.read(segmentHeaderByteArray); 304 csb.addToSegmentList(SegmentHeader.fromByteArray(segmentHeaderByteArray)); 305 } 306 // compute merkle tree offset by alignment, based on file start 307 long computedTreeOffset = getAlignmentAddr(CodeSignBlock.PAGE_SIZE_4K, fileReadOffset + offset); 308 // skip zero padding before merkle tree, adds zero padding length to fileReadOffset 309 fileReadOffset += signedHap.skip(computedTreeOffset - offset - fileReadOffset); 310 parseMerkleTree(csb, fileReadOffset, signedHap, computedTreeOffset); 311 } 312 return csb; 313 } 314 parseMerkleTree(CodeSignBlock csb, int readOffset, FileInputStream signedHap, long computedTreeOffset)315 private static void parseMerkleTree(CodeSignBlock csb, int readOffset, FileInputStream signedHap, 316 long computedTreeOffset) throws VerifyCodeSignException, IOException { 317 // check segment offset and segment size 318 byte[] merkleTreeBytes = new byte[0]; 319 int fileReadOffset = readOffset; 320 for (SegmentHeader segmentHeader : csb.getSegmentHeaderList()) { 321 if (fileReadOffset > segmentHeader.getSegmentOffset()) { 322 throw new VerifyCodeSignException("Invaild offset of merkle tree and segment header"); 323 } 324 // get merkle tree bytes 325 if (fileReadOffset < segmentHeader.getSegmentOffset()) { 326 merkleTreeBytes = new byte[segmentHeader.getSegmentOffset() - fileReadOffset]; 327 fileReadOffset += signedHap.read(merkleTreeBytes); 328 } 329 byte[] sh = new byte[segmentHeader.getSegmentSize()]; 330 fileReadOffset += signedHap.read(sh); 331 if (segmentHeader.getType() == SegmentHeader.CSB_FSVERITY_INFO_SEG) { 332 // 3) parse fs-verity info segment 333 csb.setFsVerityInfoSegment(FsVerityInfoSegment.fromByteArray(sh)); 334 } else if (segmentHeader.getType() == SegmentHeader.CSB_HAP_META_SEG) { 335 // 4) parse hap info segment 336 csb.setHapInfoSegment(HapInfoSegment.fromByteArray(sh)); 337 } else if (segmentHeader.getType() == SegmentHeader.CSB_NATIVE_LIB_INFO_SEG) { 338 // 5) parse so info segment 339 csb.setSoInfoSegment(NativeLibInfoSegment.fromByteArray(sh)); 340 } 341 } 342 if (fileReadOffset != csb.getCodeSignBlockHeader().getBlockSize()) { 343 throw new VerifyCodeSignException("Invalid blockSize of getCodeSignBlockHeader"); 344 } 345 // parse merkle tree 346 Extension extension = csb.getHapInfoSegment() 347 .getSignInfo() 348 .getExtensionByType(MerkleTreeExtension.MERKLE_TREE_INLINED); 349 if (extension == null) { 350 throw new VerifyCodeSignException("Missing merkleTreeExtension in verifycation"); 351 } 352 if (extension instanceof MerkleTreeExtension) { 353 MerkleTreeExtension mte = (MerkleTreeExtension) extension; 354 if (computedTreeOffset != mte.getMerkleTreeOffset()) { 355 throw new VerifyCodeSignException("Invalid merkle tree offset"); 356 } 357 if (merkleTreeBytes.length != mte.getMerkleTreeSize()) { 358 throw new VerifyCodeSignException("Invalid merkle tree size"); 359 } 360 csb.addOneMerkleTree(CodeSigning.HAP_SIGNATURE_ENTRY_NAME, merkleTreeBytes); 361 } 362 } 363 getAlignmentAddr(long alignment, long input)364 private static long getAlignmentAddr(long alignment, long input) { 365 long residual = input % alignment; 366 if (residual == 0) { 367 return input; 368 } else { 369 return input + (alignment - residual); 370 } 371 } 372 373 /** 374 * Verifies the signature of a given file with its signature in pkcs#7 format 375 * 376 * @param input file being verified in InputStream representation 377 * @param length size of signed data in the file 378 * @param signature byte array of signature in pkcs#7 format 379 * @param merkleTreeOffset merkle tree offset based on file start 380 * @param inMerkleTreeBytes merkle tree raw bytes 381 * @throws FsVerityDigestException if fs-verity digest generation failed 382 * @throws CMSException if signature verify failed 383 * @throws VerifyCodeSignException parsing code sign block error 384 */ verifySingleFile(InputStream input, long length, byte[] signature, long merkleTreeOffset, byte[] inMerkleTreeBytes)385 public static void verifySingleFile(InputStream input, long length, byte[] signature, long merkleTreeOffset, 386 byte[] inMerkleTreeBytes) throws FsVerityDigestException, CMSException, VerifyCodeSignException { 387 Pair<byte[], byte[]> pairResult = null; 388 try { 389 pairResult = generateFsVerityDigest(input, length, merkleTreeOffset); 390 } catch (PageInfoException e) { 391 throw new VerifyCodeSignException(e.getMessage()); 392 } 393 byte[] generatedMerkleTreeBytes = pairResult.getSecond(); 394 if (generatedMerkleTreeBytes == null) { 395 generatedMerkleTreeBytes = new byte[0]; 396 } 397 // For native libs, inMerkleTreeBytes is null, skip check here 398 if ((inMerkleTreeBytes != null) && !Arrays.equals(inMerkleTreeBytes, generatedMerkleTreeBytes)) { 399 throw new VerifyCodeSignException("verify merkle tree bytes failed"); 400 } 401 CmsUtils.verifySignDataWithUnsignedDataDigest(pairResult.getFirst(), signature); 402 } 403 generateFsVerityDigest(InputStream inputStream, long size, long merkleTreeOffset)404 private static Pair<byte[], byte[]> generateFsVerityDigest(InputStream inputStream, long size, 405 long merkleTreeOffset) throws FsVerityDigestException, PageInfoException { 406 FsVerityGenerator fsVerityGenerator = new FsVerityGenerator(); 407 fsVerityGenerator.generateFsVerityDigest(inputStream, size, merkleTreeOffset); 408 return Pair.create(fsVerityGenerator.getFsVerityDigest(), fsVerityGenerator.getTreeBytes()); 409 } 410 } 411