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     */
062    public static boolean isCatalogFile(File file) {
063        if (!file.exists() || !file.isFile()) {
064            return false;
065        }
066
067        try {
068            CatalogFile catFile = new CatalogFile(file);
069            catFile.close();
070            return true;
071        } catch (IOException e) {
072            return false;
073        }
074    }
075
076    /**
077     * Create a Windows catalog from the specified file.
078     *
079     * @param file the file to open
080     * @throws IOException if an I/O error occurs
081     */
082    public CatalogFile(File file) throws IOException {
083        this(Files.newByteChannel(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE));
084    }
085
086    /**
087     * Create a Windows catalog from the specified channel.
088     *
089     * @param channel the channel to read the file from
090     * @throws IOException if an I/O error occurs
091     */
092    public CatalogFile(SeekableByteChannel channel) throws IOException {
093        this.channel = channel;
094
095        channel.position(0);
096
097        try {
098            signedData = new CMSSignedData(Channels.newInputStream(channel));
099        } catch (CMSException e) {
100            throw new IOException("Catalog file format error", e);
101        }
102    }
103
104    @Override
105    public void close() throws IOException {
106        channel.close();
107    }
108
109    @Override
110    public ContentInfo createContentInfo(DigestAlgorithm digestAlgorithm) {
111        return new ContentInfo(signedData.getSignedContent().getContentType(), (ASN1Encodable) signedData.getSignedContent().getContent());
112    }
113
114    @Override
115    public byte[] computeDigest(MessageDigest digest) {
116        throw new UnsupportedOperationException();
117    }
118
119    @Override
120    public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) {
121        throw new UnsupportedOperationException();
122    }
123
124    @Override
125    public List<CMSSignedData> getSignatures() throws IOException {
126        List<CMSSignedData> signatures = new ArrayList<>();
127
128        try {
129            if (signedData.getSignerInfos().size() > 0) {
130                signatures.add(signedData);
131
132                // look for nested signatures
133                SignerInformation signerInformation = signedData.getSignerInfos().getSigners().iterator().next();
134                AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
135                if (unsignedAttributes != null) {
136                    Attribute nestedSignatures = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID);
137                    if (nestedSignatures != null) {
138                        for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) {
139                            signatures.add(new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(nestedSignature)));
140                        }
141                    }
142                }
143            }
144        } catch (CMSException e) {
145            throw new IOException(e);
146        }
147
148        return signatures;
149    }
150
151    @Override
152    public void setSignature(CMSSignedData signature) {
153        if (signature != null) {
154            signedData = signature;
155        }
156    }
157
158    @Override
159    public void save() throws IOException {
160        channel.position(0);
161        channel.truncate(0);
162        channel.write(ByteBuffer.wrap(signedData.getEncoded("DER")));
163    }
164}