001/** 002 * Copyright 2022 Emmanuel Bourg and contributors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package net.jsign.cat; 018 019import java.io.File; 020import java.io.IOException; 021import java.nio.ByteBuffer; 022import java.nio.channels.Channels; 023import java.nio.channels.SeekableByteChannel; 024import java.nio.file.Files; 025import java.nio.file.StandardOpenOption; 026import java.security.MessageDigest; 027import java.util.ArrayList; 028import java.util.List; 029 030import org.bouncycastle.asn1.ASN1Encodable; 031import org.bouncycastle.asn1.ASN1Object; 032import org.bouncycastle.asn1.cms.Attribute; 033import org.bouncycastle.asn1.cms.AttributeTable; 034import org.bouncycastle.asn1.cms.ContentInfo; 035import org.bouncycastle.cms.CMSException; 036import org.bouncycastle.cms.CMSProcessable; 037import org.bouncycastle.cms.CMSSignedData; 038import org.bouncycastle.cms.SignerInformation; 039 040import net.jsign.DigestAlgorithm; 041import net.jsign.Signable; 042import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; 043 044/** 045 * Windows Catalog file. 046 * 047 * @see <a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/install/catalog-files">Windows Drivers - Catalog Files and Digital Signatures</a> 048 * @since 4.2 049 */ 050public class CatalogFile implements Signable { 051 052 private final SeekableByteChannel channel; 053 054 private CMSSignedData signedData; 055 056 /** 057 * Tells if the specified file is a Windows catalog file. 058 * 059 * @param file the file to check 060 * @return <code>true</code> if the file is a Windows catalog, <code>false</code> otherwise 061 * @throws IOException if an I/O error occurs 062 */ 063 public static boolean isCatalogFile(File file) { 064 if (!file.exists() || !file.isFile()) { 065 return false; 066 } 067 068 try { 069 CatalogFile catFile = new CatalogFile(file); 070 catFile.close(); 071 return true; 072 } catch (IOException e) { 073 return false; 074 } 075 } 076 077 /** 078 * Create a Windows catalog from the specified file. 079 * 080 * @param file the file to open 081 * @throws IOException if an I/O error occurs 082 */ 083 public CatalogFile(File file) throws IOException { 084 this(Files.newByteChannel(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE)); 085 } 086 087 /** 088 * Create a Windows catalog from the specified channel. 089 * 090 * @param channel the channel to read the file from 091 * @throws IOException if an I/O error occurs 092 */ 093 public CatalogFile(SeekableByteChannel channel) throws IOException { 094 this.channel = channel; 095 096 channel.position(0); 097 098 try { 099 signedData = new CMSSignedData(Channels.newInputStream(channel)); 100 } catch (CMSException e) { 101 throw new IOException("Catalog file format error", e); 102 } 103 } 104 105 @Override 106 public void close() throws IOException { 107 channel.close(); 108 } 109 110 @Override 111 public ContentInfo createContentInfo(DigestAlgorithm digestAlgorithm) { 112 return new ContentInfo(signedData.getSignedContent().getContentType(), (ASN1Encodable) signedData.getSignedContent().getContent()); 113 } 114 115 @Override 116 public byte[] computeDigest(MessageDigest digest) { 117 throw new UnsupportedOperationException(); 118 } 119 120 @Override 121 public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) { 122 throw new UnsupportedOperationException(); 123 } 124 125 @Override 126 public List<CMSSignedData> getSignatures() throws IOException { 127 List<CMSSignedData> signatures = new ArrayList<>(); 128 129 try { 130 if (signedData.getSignerInfos().size() > 0) { 131 signatures.add(signedData); 132 133 // look for nested signatures 134 SignerInformation signerInformation = signedData.getSignerInfos().getSigners().iterator().next(); 135 AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); 136 if (unsignedAttributes != null) { 137 Attribute nestedSignatures = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID); 138 if (nestedSignatures != null) { 139 for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) { 140 signatures.add(new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(nestedSignature))); 141 } 142 } 143 } 144 } 145 } catch (CMSException e) { 146 throw new IOException(e); 147 } 148 149 return signatures; 150 } 151 152 @Override 153 public void setSignature(CMSSignedData signature) { 154 if (signature != null) { 155 signedData = signature; 156 } 157 } 158 159 @Override 160 public void save() throws IOException { 161 channel.position(0); 162 channel.truncate(0); 163 channel.write(ByteBuffer.wrap(signedData.getEncoded("DER"))); 164 } 165}