SignatureUtils.java

/*
 * Copyright 2024 Emmanuel Bourg
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.jsign;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.CMSAttributes;
import org.bouncycastle.asn1.cms.ContentInfo;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;

import static net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers.*;

/**
 * Helper class for working with signatures.
 *
 * @since 7.0
 */
public class SignatureUtils {

    /**
     * Parse the specified signature and return the nested Authenticode signatures.
     *
     * @param signature the signature to analyze
     */
    public static List<CMSSignedData> getSignatures(byte[] signature) throws IOException {
        try (ASN1InputStream in = new ASN1InputStream(signature)) {
            CMSSignedData signedData = new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(in.readObject()));
            return getSignatures(signedData);
        } catch (CMSException | IllegalArgumentException | IllegalStateException | NoSuchElementException | ClassCastException | StackOverflowError e) {
            throw new IOException(e);
        }
    }

    /**
     * Extract the nested Authenticode signatures from the specified signature.
     *
     * @param signature the signature to analyze
     */
    public static List<CMSSignedData> getSignatures(CMSSignedData signature) throws IOException {
        List<CMSSignedData> signatures = new ArrayList<>();

        try {
            if (signature != null) {
                signatures.add(signature);

                // look for nested signatures
                SignerInformation signerInformation = signature.getSignerInfos().iterator().next();
                AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
                if (unsignedAttributes != null) {
                    Attribute nestedSignatures = unsignedAttributes.get(SPC_NESTED_SIGNATURE_OBJID);
                    if (nestedSignatures != null) {
                        for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) {
                            signatures.add(new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(nestedSignature)));
                        }
                    }
                }
            }
        } catch (CMSException e) {
            throw new IOException(e);
        }

        return signatures;
    }

    /**
     * Embed a signature as an unsigned attribute of an existing signature.
     *
     * @param parent    the root signature hosting the nested secondary signature
     * @param children  the additional signature to nest inside the root signature
     * @return the signature combining the specified signatures
     */
    static CMSSignedData addNestedSignature(CMSSignedData parent, boolean replace, CMSSignedData... children) {
        SignerInformation signerInformation = parent.getSignerInfos().iterator().next();

        AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
        if (unsignedAttributes == null) {
            unsignedAttributes = new AttributeTable(new DERSet());
        }

        Attribute nestedSignaturesAttribute = unsignedAttributes.get(SPC_NESTED_SIGNATURE_OBJID);
        ASN1EncodableVector nestedSignatures = new ASN1EncodableVector();
        if (nestedSignaturesAttribute != null && !replace) {
            // keep the previous nested signatures
            for (ASN1Encodable nestedSignature : nestedSignaturesAttribute.getAttrValues()) {
                nestedSignatures.add(nestedSignature);
            }
        }

        // append the new signatures
        for (CMSSignedData nestedSignature : children) {
            nestedSignatures.add(nestedSignature.toASN1Structure());
        }

        // replace the nested signatures attribute
        ASN1EncodableVector attributes = unsignedAttributes.remove(SPC_NESTED_SIGNATURE_OBJID).toASN1EncodableVector();
        attributes.add(new Attribute(SPC_NESTED_SIGNATURE_OBJID, new DERSet(nestedSignatures)));

        unsignedAttributes = new AttributeTable(attributes);

        signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes);
        return CMSSignedData.replaceSigners(parent, new SignerInformationStore(signerInformation));
    }

    /**
     * Tells if the specified signature is timestamped.
     *
     * @param signature the signature to check
     */
    static boolean isTimestamped(CMSSignedData signature) {
        SignerInformation signerInformation = signature.getSignerInfos().iterator().next();

        AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
        if (unsignedAttributes == null) {
            return false;
        }

        boolean authenticode = isAuthenticode(signature.getSignedContentTypeOID());
        Attribute authenticodeTimestampAttribute = unsignedAttributes.get(CMSAttributes.counterSignature);
        Attribute rfc3161TimestampAttribute = unsignedAttributes.get(authenticode ? SPC_RFC3161_OBJID : PKCSObjectIdentifiers.id_aa_signatureTimeStampToken);
        return authenticodeTimestampAttribute != null || rfc3161TimestampAttribute != null;
    }

    /**
     * Removes the timestamp from the specified signature.
     *
     * @param signature the signature to modify
     */
    static CMSSignedData removeTimestamp(CMSSignedData signature) {
        SignerInformation signerInformation = signature.getSignerInfos().iterator().next();

        AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
        if (unsignedAttributes == null) {
            return signature;
        }

        unsignedAttributes = unsignedAttributes.remove(CMSAttributes.counterSignature);
        unsignedAttributes = unsignedAttributes.remove(PKCSObjectIdentifiers.id_aa_signatureTimeStampToken);
        unsignedAttributes = unsignedAttributes.remove(SPC_RFC3161_OBJID);

        // todo remove the TSA certificates from the certificate store

        signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes);
        return CMSSignedData.replaceSigners(signature, new SignerInformationStore(signerInformation));
    }
}