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}