001/*
002 * Copyright 2019 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;
018
019import java.io.Closeable;
020import java.io.File;
021import java.io.IOException;
022import java.nio.charset.Charset;
023import java.security.MessageDigest;
024import java.security.cert.Certificate;
025import java.security.cert.CertificateEncodingException;
026import java.security.cert.X509Certificate;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.List;
030import java.util.ServiceLoader;
031import java.util.function.Supplier;
032import java.util.stream.Stream;
033
034import org.bouncycastle.asn1.ASN1Object;
035import org.bouncycastle.asn1.cms.Attribute;
036import org.bouncycastle.asn1.cms.ContentInfo;
037import org.bouncycastle.cms.CMSSignedData;
038import org.bouncycastle.cms.CMSTypedData;
039import org.bouncycastle.cms.PKCS7ProcessableObject;
040
041import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers;
042import net.jsign.spi.SignableProvider;
043
044/**
045 * A file that can be signed with Authenticode.
046 *
047 * @author Emmanuel Bourg
048 */
049public interface Signable extends Closeable {
050
051    /**
052     * Creates the ContentInfo or EncapsulatedContentInfo structure to be signed.
053     *
054     * @param digestAlgorithm the digest algorithm to use
055     * @return the ContentInfo or EncapsulatedContentInfo structure
056     * @throws IOException if an I/O error occurs
057     * @since 7.0
058     */
059    default CMSTypedData createSignedContent(DigestAlgorithm digestAlgorithm) throws IOException {
060        return new PKCS7ProcessableObject(AuthenticodeObjectIdentifiers.SPC_INDIRECT_DATA_OBJID, createIndirectData(digestAlgorithm));
061    }
062
063    /**
064     * Creates the ContentInfo structure to be signed.
065     *
066     * @param digestAlgorithm the digest algorithm to use
067     * @return the ContentInfo structure in ASN.1 format
068     * @throws IOException if an I/O error occurs
069     * @since 4.2
070     * @deprecated Use {@link #createSignedContent(DigestAlgorithm)} instead
071     */
072    default ContentInfo createContentInfo(DigestAlgorithm digestAlgorithm) throws IOException {
073        return new ContentInfo(AuthenticodeObjectIdentifiers.SPC_INDIRECT_DATA_OBJID, createIndirectData(digestAlgorithm));
074    }
075
076    /**
077     * Computes the digest of the file.
078     * 
079     * @param digest the message digest to update
080     * @return the digest of the file
081     * @throws IOException if an I/O error occurs
082     * @deprecated Use {@link #computeDigest(DigestAlgorithm)} instead
083     */
084    default byte[] computeDigest(MessageDigest digest) throws IOException {
085        return computeDigest(DigestAlgorithm.of(digest.getAlgorithm()));
086    }
087
088    /**
089     * Computes the digest of the file.
090     *
091     * @param digestAlgorithm the digest algorithm to use
092     * @return the digest of the file
093     * @throws IOException if an I/O error occurs
094     * @since 6.0
095     */
096    default byte[] computeDigest(DigestAlgorithm digestAlgorithm) throws IOException {
097        return computeDigest(digestAlgorithm.getMessageDigest());
098    }
099
100    /**
101     * Creates the SpcIndirectDataContent structure containing the digest of the file.
102     * 
103     * @param digestAlgorithm the digest algorithm to use
104     * @return the SpcIndirectDataContent structure in ASN.1 format
105     * @throws IOException if an I/O error occurs
106     */
107    ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException;
108
109    /**
110     * Creates the signed attributes to include in the signature.
111     *
112     * @param certificate the signing certificate
113     * @since 7.0
114     */
115    default List<Attribute> createSignedAttributes(X509Certificate certificate) throws CertificateEncodingException {
116        return new ArrayList<>();
117    }
118
119    /**
120     * Checks if the specified certificate is suitable for signing the file.
121     *
122     * @param certificate the certificate to validate
123     * @throws IOException if an I/O error occurs
124     * @throws IllegalArgumentException if the certificate doesn't match the publisher identity
125     * @since 7.0
126     */
127    default void validate(Certificate certificate) throws IOException, IllegalArgumentException {
128    }
129
130    /**
131     * Returns the Authenticode signatures on the file. To modify the signatures, use {@link #setSignatures(List)}.
132     * 
133     * @return the signatures (empty if the file is not signed)
134     * @throws IOException if an I/O error occurs
135     */
136    List<CMSSignedData> getSignatures() throws IOException;
137
138    /**
139     * Sets the signature of the file, overwriting the previous one.
140     * 
141     * @param signature the signature to put, or null to remove the signature
142     * @throws IOException if an I/O error occurs
143     */
144    void setSignature(CMSSignedData signature) throws IOException;
145
146    /**
147     * Sets the signatures of the file, overwriting the previous ones.
148     * The default implementation folds all the signatures into the first one (using a SPC_NESTED_SIGNATURE_OBJID
149     * unauthenticated attribute) and then calls {@link #setSignature(CMSSignedData)}.
150     *
151     * @param signatures the signatures to put, empty or null to remove the signatures
152     * @throws IOException if an I/O error occurs
153     * @since 7.3
154     */
155    default void setSignatures(List<CMSSignedData> signatures) throws IOException {
156        if (signatures == null || signatures.isEmpty()) {
157            setSignature(null);
158        } else {
159            CMSSignedData signature = signatures.get(0);
160            if (signatures.size() > 1) {
161                List<CMSSignedData> nestedSignatures = signatures.subList(1, signatures.size());
162                signature = SignatureUtils.addNestedSignature(signature, true, nestedSignatures.toArray(new CMSSignedData[0]));
163            }
164            setSignature(signature);
165        }
166    }
167
168    /**
169     * Saves the file.
170     * 
171     * @throws IOException if an I/O error occurs
172     */
173    void save() throws IOException;
174
175    /**
176     * Returns a signable object for the file specified.
177     *
178     * @param file the file that is intended to be signed
179     * @return the signable object for the specified file
180     * @throws IOException if an I/O error occurs
181     * @throws UnsupportedOperationException if the file specified isn't supported
182     */
183    static Signable of(File file) throws IOException {
184        return of(file, null);
185    }
186
187    /**
188     * Returns a signable object for the file specified.
189     *
190     * @param file     the file that is intended to be signed
191     * @param encoding the character encoding (for text files only).
192     *                 If the file has a byte order mark this parameter is ignored.
193     * @return the signable object for the specified file
194     * @throws IOException if an I/O error occurs
195     * @throws UnsupportedOperationException if the file specified isn't supported
196     */
197    static Signable of(File file, Charset encoding) throws IOException {
198        // look for SignableProvider implementations in the classloader that loaded the Jsign classes and in the current classloader
199        Supplier<ServiceLoader<SignableProvider>> loaders1 = () -> ServiceLoader.load(SignableProvider.class, Signable.class.getClassLoader());
200        Supplier<ServiceLoader<SignableProvider>> loaders2 = () -> ServiceLoader.load(SignableProvider.class);
201
202        for (Supplier<ServiceLoader<SignableProvider>> loaders : Arrays.asList(loaders1, loaders2)) {
203            for (SignableProvider provider : loaders.get()) {
204                if (provider.isSupported(file)) {
205                    return provider.create(file, encoding);
206                }
207            }
208        }
209
210        throw new UnsupportedOperationException("Unsupported file: " + file);
211    }
212}