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.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.NoSuchElementException;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
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.cms.Time;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.tsp.TSTInfo;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.DigestInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.selector.X509CertificateHolderSelector;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.util.Store;

import net.jsign.asn1.authenticode.SpcSpOpusInfo;

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

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

    /**
     * Parse the specified signature.
     *
     * @param signature the signature to analyze
     * @since 8.0
     */
    public static CMSSignedData getSignature(byte[] signature) throws IOException {
        try {
            return new CMSSignedData(signature);
        } catch (CMSException | IllegalArgumentException | IllegalStateException | NoSuchElementException | ClassCastException | StackOverflowError e) {
            // Bouncy Castle can throw a wide range of exceptions when parsing a signature, so we wrap them all in an IOException
            throw new IOException("Malformed signature", e);
        }
    }

    /**
     * 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 {
        return getSignatures(getSignature(signature));
    }

    /**
     * Extract the nested Authenticode signatures from the specified signature.
     *
     * @param signature the signature to analyze
     * @return the list of signatures (the first one is the parent signature without nested signatures)
     */
    public static List<CMSSignedData> getSignatures(CMSSignedData signature) throws IOException {
        List<CMSSignedData> signatures = new ArrayList<>();

        try {
            if (signature != null) {
                signatures.add(signature);
                signatures.set(0, SignatureUtils.removeNestedSignatures(signature));

                // look for nested signatures
                Attribute nestedSignatures = getUnsignedAttribute(signature, SPC_NESTED_SIGNATURE_OBJID);
                if (nestedSignatures != null) {
                    for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) {
                        signatures.add(new CMSSignedData(nestedSignature.toASN1Primitive().getEncoded()));
                    }
                }
            }
        } 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));
    }

    /**
     * Remove the nested signatures from the specified signature.
     *
     * @param signature the signature to modify
     * @return the signature without nested signatures
     */
    static CMSSignedData removeNestedSignatures(CMSSignedData signature) {
        return removeUnsignedAttributes(signature, SPC_NESTED_SIGNATURE_OBJID);
    }

    /**
     * Tells if the specified signature is timestamped.
     *
     * @param signature the signature to check
     */
    static boolean isTimestamped(CMSSignedData signature) {
        boolean authenticode = isAuthenticode(signature.getSignedContentTypeOID());
        Attribute authenticodeTimestampAttribute = getUnsignedAttribute(signature, CMSAttributes.counterSignature);
        Attribute rfc3161TimestampAttribute = getUnsignedAttribute(signature, 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) {
        // todo remove the TSA certificates from the certificate store

        return removeUnsignedAttributes(signature,
                CMSAttributes.counterSignature,
                PKCSObjectIdentifiers.id_aa_signatureTimeStampToken,
                SPC_RFC3161_OBJID);
    }

    /**
     * Remove the specified unsigned attributes from the signature.
     *
     * @param signature the signature to modify
     * @param oids the OIDs of the attributes to remove
     * @return the modified signature
     */
    static CMSSignedData removeUnsignedAttributes(CMSSignedData signature, ASN1ObjectIdentifier... oids) {
        SignerInformation signerInformation = signature.getSignerInfos().iterator().next();

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

        for (ASN1ObjectIdentifier oid : oids) {
            unsignedAttributes = unsignedAttributes.remove(oid);
        }

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

    /**
     * Returns the digest info of the signature.
     *
     * @since 8.0
     */
    public static DigestInfo getDigestInfo(CMSSignedData signature) {
        if (SPC_INDIRECT_DATA_OBJID.equals(signature.getSignedContent().getContentType())) {
            ASN1Sequence indirectData = ASN1Sequence.getInstance(signature.getSignedContent().getContent());
            return DigestInfo.getInstance(indirectData.getObjectAt(1));
        }

        if (PKCSObjectIdentifiers.data.equals(signature.getSignedContent().getContentType())) {
            // the data is assumed to be the digest of the file with the same algorithm as the signature
            SignerInformation signer = signature.getSignerInfos().iterator().next();
            AlgorithmIdentifier digestAlgorithm = signer.getDigestAlgorithmID();
            return new DigestInfo(digestAlgorithm, (byte[]) signature.getSignedContent().getContent());
        }

        return null;
    }

    /**
     * Returns the SpcSpOpusInfo in the authenticated attributes of the signature.
     *
     * @since 8.0
     */
    static SpcSpOpusInfo getSpcSpOpusInfo(CMSSignedData signature) {
        SignerInformation signerInformation = signature.getSignerInfos().iterator().next();
        Attribute attribute = signerInformation.getSignedAttributes().get(SPC_SP_OPUS_INFO_OBJID);
        return attribute != null ? SpcSpOpusInfo.parse(attribute.getAttrValues().getObjectAt(0)) : null;
    }

    /**
     * Returns the timestamp signer information.
     *
     * @since 8.0
     */
    static SignerInformation getCounterSigner(CMSSignedData signature) throws CMSException {
        SignerInformation signer = signature.getSignerInfos().iterator().next();

        Collection<SignerInformation> counterSigners = Collections.emptyList();

        Attribute timestampAttribute = getUnsignedAttribute(signature, CMSAttributes.counterSignature);
        if (timestampAttribute != null) {
            counterSigners = signer.getCounterSignatures().getSigners();
        }

        timestampAttribute  = getUnsignedAttribute(signature, SPC_RFC3161_OBJID);
        if (timestampAttribute == null) {
            timestampAttribute = getUnsignedAttribute(signature, PKCSObjectIdentifiers.id_aa_signatureTimeStampToken);
        }
        if (timestampAttribute != null) {
            CMSSignedData signedData = new CMSSignedData(ContentInfo.getInstance(timestampAttribute.getAttrValues().getObjectAt(0)));
            counterSigners = signedData.getSignerInfos().getSigners();
        }

        return !counterSigners.isEmpty() ? counterSigners.iterator().next() : null;
    }

    /**
     * Returns the signing time of the timestamp.
     *
     * @since 8.0
     */
    static Date getTimestampDate(CMSSignedData signature) throws CMSException, ParseException {
        SignerInformation counterSigner = getCounterSigner(signature);
        if (counterSigner != null) {
            Attribute signingTime = counterSigner.getSignedAttributes().get(CMSAttributes.signingTime);
            if (signingTime != null) {
                return Time.getInstance(signingTime.getAttrValues().getObjectAt(0)).getDate();
            }
        }

        Attribute timestampAttribute = getUnsignedAttribute(signature, SPC_RFC3161_OBJID);
        if (timestampAttribute == null) {
            timestampAttribute = getUnsignedAttribute(signature, PKCSObjectIdentifiers.id_aa_signatureTimeStampToken);
        }
        if (timestampAttribute != null) {
            CMSSignedData signedData = new CMSSignedData(ContentInfo.getInstance(timestampAttribute.getAttrValues().getObjectAt(0)));

            TSTInfo timestampTokenInfo = TSTInfo.getInstance(signedData.getSignedContent().getContent());
            return timestampTokenInfo.getGenTime().getDate();
        }

        return null;
    }

    /**
     * Returns the certificate of the timestamp.
     *
     * @since 8.0
     */
    static X509CertificateHolder getTimestampCertificate(CMSSignedData signature) throws CMSException {
        SignerInformation counterSigner = getCounterSigner(signature);
        if (counterSigner == null) {
            return null;
        }

        Store<X509CertificateHolder> certificates = signature.getCertificates();
        AttributeTable unsignedAttributes = signature.getSignerInfos().iterator().next().getUnsignedAttributes();
        Attribute timestampAttribute  = unsignedAttributes.get(SPC_RFC3161_OBJID);
        if (timestampAttribute != null) {
            CMSSignedData signedData = new CMSSignedData(ContentInfo.getInstance(timestampAttribute.getAttrValues().getObjectAt(0)));
            certificates = signedData.getCertificates();
        }

        X509CertificateHolderSelector selector = new X509CertificateHolderSelector(counterSigner.getSID().getIssuer(), counterSigner.getSID().getSerialNumber());

        Collection<X509CertificateHolder> matches = certificates.getMatches(selector);
        return !matches.isEmpty() ? matches.iterator().next() : null;
    }


    /**
     * Returns the value of the unsigned tag
     *
     * @param signature the CMS signed data
     * @return the value of the unsigned tag (ASN1UTF8String or ASN1OctetString), or null if not found
     * @since 8.0
     */
    static ASN1Encodable getTag(CMSSignedData signature) throws IOException {
        Attribute attribute = getUnsignedAttribute(signature, JSIGN_UNSIGNED_DATA_OBJID);
        if (attribute == null) {
            attribute = getUnsignedAttribute(signature, new ASN1ObjectIdentifier("1.3.6.1.4.1.42921.1.2.1")); // Dropbox OID used by osslsigncode
        }

        return attribute != null ? attribute.getAttrValues().getObjectAt(0) : null;
    }

    /**
     * Returns the specified unsigned attribute from the signature.
     *
     * @param signature the signature
     * @param oid       the object identifier of the attribute
     * @return the unsigned attribute, or null if not found
     * @since 8.0
     */
    static Attribute getUnsignedAttribute(CMSSignedData signature, ASN1ObjectIdentifier oid) {
        SignerInformation signer = signature.getSignerInfos().iterator().next();

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

        return unsignedAttributes.get(oid);
    }

    /**
     * Adds an unsigned attribute to the signature/
     *
     * @param signature the signature
     * @param oid       the object identifier of the attribute
     * @param value     the value of the attribute
     * @since 8.0
     */
    static CMSSignedData addUnsignedAttribute(CMSSignedData signature, ASN1ObjectIdentifier oid, ASN1Encodable value) {
        SignerInformationStore store = signature.getSignerInfos();
        Collection<SignerInformation> signers = store.getSigners();
        SignerInformation signer = signers.iterator().next();

        AttributeTable attributes = signer.getUnsignedAttributes();
        if (attributes == null) {
            attributes = new AttributeTable(new DERSet());
        }
        attributes = attributes.add(oid, value);

        signers.remove(signer);
        signers.add(SignerInformation.replaceUnsignedAttributes(signer, attributes));
        return CMSSignedData.replaceSigners(signature, new SignerInformationStore(signers));
    }
}