/*
 * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.security.pkcs;

import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.*;
import java.security.spec.PSSParameterSpec;
import java.util.*;

import sun.security.provider.SHA3.SHAKE256;
import sun.security.timestamp.TimestampToken;
import sun.security.util.*;
import sun.security.x509.AlgorithmId;
import sun.security.x509.KeyUsageExtension;
import sun.security.x509.X500Name;

/**
 * A SignerInfo, as defined in PKCS#7's signedData type.
 *
 * @author Benjamin Renaud
 */
public class SignerInfo implements DerEncoder {

    private static final DisabledAlgorithmConstraints JAR_DISABLED_CHECK =
            DisabledAlgorithmConstraints.jarConstraints();

    BigInteger version;
    X500Name issuerName;
    BigInteger certificateSerialNumber;
    AlgorithmId digestAlgorithmId;
    AlgorithmId digestEncryptionAlgorithmId;
    byte[] encryptedDigest;
    Timestamp timestamp;
    private boolean hasTimestamp = true;
    private static final Debug debug = Debug.getInstance("jar");

    PKCS9Attributes authenticatedAttributes;
    PKCS9Attributes unauthenticatedAttributes;

    /**
     * A map containing the algorithms in this SignerInfo. This is used to
     * avoid checking algorithms to see if they are disabled more than once.
     * The key is the AlgorithmId of the algorithm, and the value is a record
     * containing the name of the field or attribute and whether the key
     * should also be checked (ex: if it is a signature algorithm).
     */
    private record AlgorithmInfo(String field, boolean checkKey) {}
    private final Map<AlgorithmId, AlgorithmInfo> algorithms = new HashMap<>();

    public SignerInfo(X500Name  issuerName,
                      BigInteger serial,
                      AlgorithmId digestAlgorithmId,
                      AlgorithmId digestEncryptionAlgorithmId,
                      byte[] encryptedDigest) {
        this(issuerName, serial, digestAlgorithmId, null,
                digestEncryptionAlgorithmId, encryptedDigest, null);
    }

    public SignerInfo(X500Name  issuerName,
                      BigInteger serial,
                      AlgorithmId digestAlgorithmId,
                      PKCS9Attributes authenticatedAttributes,
                      AlgorithmId digestEncryptionAlgorithmId,
                      byte[] encryptedDigest,
                      PKCS9Attributes unauthenticatedAttributes) {
        this.version = BigInteger.ONE;
        this.issuerName = issuerName;
        this.certificateSerialNumber = serial;
        this.digestAlgorithmId = digestAlgorithmId;
        this.authenticatedAttributes = authenticatedAttributes;
        this.digestEncryptionAlgorithmId = digestEncryptionAlgorithmId;
        this.encryptedDigest = encryptedDigest;
        this.unauthenticatedAttributes = unauthenticatedAttributes;
    }

    /**
     * Parses a PKCS#7 signer info.
     */
    public SignerInfo(DerInputStream derin) throws IOException {
        this(derin, false);
    }

    /**
     * Parses a PKCS#7 signer info.
     *
     * <p>This constructor is used only for backwards compatibility with
     * PKCS#7 blocks that were generated using JDK1.1.x.
     *
     * @param derin the ASN.1 encoding of the signer info.
     * @param oldStyle flag indicating whether the given signer info
     * is encoded according to JDK1.1.x.
     */
    public SignerInfo(DerInputStream derin, boolean oldStyle)
            throws IOException {
        // version
        version = derin.getBigInteger();

        // issuerAndSerialNumber
        DerValue[] issuerAndSerialNumber = derin.getSequence(2);
        if (issuerAndSerialNumber.length != 2) {
            throw new ParsingException("Invalid length for IssuerAndSerialNumber");
        }
        byte[] issuerBytes = issuerAndSerialNumber[0].toByteArray();
        issuerName = new X500Name(new DerValue(DerValue.tag_Sequence,
                                               issuerBytes));
        certificateSerialNumber = issuerAndSerialNumber[1].getBigInteger();

        // digestAlgorithmId
        DerValue tmp = derin.getDerValue();

        digestAlgorithmId = AlgorithmId.parse(tmp);

        // authenticatedAttributes
        if (oldStyle) {
            // In JDK1.1.x, the authenticatedAttributes are always present,
            // encoded as an empty Set (Set of length zero)
            derin.getSet(0);
        } else {
            // check if set of auth attributes (implicit tag) is provided
            // (auth attributes are OPTIONAL)
            if ((byte)(derin.peekByte()) == (byte)0xA0) {
                authenticatedAttributes = new PKCS9Attributes(derin);
            }
        }

        // digestEncryptionAlgorithmId - little RSA naming scheme -
        // signature == encryption...
        tmp = derin.getDerValue();

        digestEncryptionAlgorithmId = AlgorithmId.parse(tmp);

        // encryptedDigest
        encryptedDigest = derin.getOctetString();

        // unauthenticatedAttributes
        if (oldStyle) {
            // In JDK1.1.x, the unauthenticatedAttributes are always present,
            // encoded as an empty Set (Set of length zero)
            derin.getSet(0);
        } else {
            // check if set of unauth attributes (implicit tag) is provided
            // (unauth attributes are OPTIONAL)
            if (derin.available() != 0
                && (byte)(derin.peekByte()) == (byte)0xA1) {
                unauthenticatedAttributes =
                    new PKCS9Attributes(derin, true);// ignore unsupported attrs
            }
        }

        // all done
        if (derin.available() != 0) {
            throw new ParsingException("extra data at the end");
        }

        // verify CMSAlgorithmProtection
        checkCMSAlgorithmProtection();
    }

    // CMSAlgorithmProtection verification as described in RFC 6211
    private void checkCMSAlgorithmProtection() throws IOException {
        if (authenticatedAttributes == null) {
            return;
        }
        PKCS9Attribute ap = authenticatedAttributes.getAttribute(
                PKCS9Attribute.CMS_ALGORITHM_PROTECTION_OID);
        if (ap == null) {
            return;
        }
        DerValue dv = new DerValue((byte[])ap.getValue());
        DerInputStream data = dv.data();
        AlgorithmId d = AlgorithmId.parse(data.getDerValue());
        DerValue ds = data.getDerValue();
        if (data.available() > 0) {
            throw new IOException("Unknown field in CMSAlgorithmProtection");
        }
        if (!ds.isContextSpecific((byte)1)) {
            throw new IOException("No signature algorithm in CMSAlgorithmProtection");
        }
        AlgorithmId s = AlgorithmId.parse(ds.withTag(DerValue.tag_Sequence));
        if (!s.equals(digestEncryptionAlgorithmId)
                || !d.equals(digestAlgorithmId)) {
            throw new IOException("CMSAlgorithmProtection check failed");
        }
    }

    /**
     * DER encode this object onto an output stream.
     * Implements the {@code DerEncoder} interface.
     *
     * @param out the output stream on which to write the DER encoding.
     */
    @Override
    public void encode(DerOutputStream out) {
        DerOutputStream seq = new DerOutputStream();
        seq.putInteger(version);
        DerOutputStream issuerAndSerialNumber = new DerOutputStream();
        issuerName.encode(issuerAndSerialNumber);
        issuerAndSerialNumber.putInteger(certificateSerialNumber);
        seq.write(DerValue.tag_Sequence, issuerAndSerialNumber);

        digestAlgorithmId.encode(seq);

        // encode authenticated attributes if there are any
        if (authenticatedAttributes != null)
            authenticatedAttributes.encode((byte)0xA0, seq);

        digestEncryptionAlgorithmId.encode(seq);

        seq.putOctetString(encryptedDigest);

        // encode unauthenticated attributes if there are any
        if (unauthenticatedAttributes != null)
            unauthenticatedAttributes.encode((byte)0xA1, seq);

        out.write(DerValue.tag_Sequence, seq);
    }

    /*
     * Returns the (user) certificate pertaining to this SignerInfo.
     */
    public X509Certificate getCertificate(PKCS7 block)
        throws IOException
    {
        return block.getCertificate(certificateSerialNumber, issuerName);
    }

    /*
     * Returns the certificate chain pertaining to this SignerInfo.
     */
    public ArrayList<X509Certificate> getCertificateChain(PKCS7 block)
        throws IOException
    {
        X509Certificate userCert;
        userCert = block.getCertificate(certificateSerialNumber, issuerName);
        if (userCert == null)
            return null;

        ArrayList<X509Certificate> certList = new ArrayList<>();
        certList.add(userCert);

        X509Certificate[] pkcsCerts = block.getCertificates();
        if (pkcsCerts == null
            || userCert.getSubjectX500Principal().equals(userCert.getIssuerX500Principal())) {
            return certList;
        }

        Principal issuer = userCert.getIssuerX500Principal();
        int start = 0;
        while (true) {
            boolean match = false;
            int i = start;
            while (i < pkcsCerts.length) {
                if (issuer.equals(pkcsCerts[i].getSubjectX500Principal())) {
                    // next cert in chain found
                    certList.add(pkcsCerts[i]);
                    // if selected cert is self-signed, we're done
                    // constructing the chain
                    if (pkcsCerts[i].getSubjectX500Principal().equals(
                                            pkcsCerts[i].getIssuerX500Principal())) {
                        start = pkcsCerts.length;
                    } else {
                        issuer = pkcsCerts[i].getIssuerX500Principal();
                        X509Certificate tmpCert = pkcsCerts[start];
                        pkcsCerts[start] = pkcsCerts[i];
                        pkcsCerts[i] = tmpCert;
                        start++;
                    }
                    match = true;
                    break;
                } else {
                    i++;
                }
            }
            if (!match)
                break;
        }

        return certList;
    }

    /* Returns null if verify fails, this signerInfo if
       verify succeeds. */
    SignerInfo verify(PKCS7 block, byte[] data)
    throws NoSuchAlgorithmException, SignatureException {

        try {
            Timestamp timestamp = null;
            try {
                timestamp = getTimestamp();
            } catch (Exception e) {
                // Log exception and continue. This allows for the case
                // where, if there are no other errors, the code is
                // signed but w/o a timestamp.
                if (debug != null) {
                    debug.println("Unexpected exception while getting" +
                                  " timestamp: " + e);
                }
            }

            ContentInfo content = block.getContentInfo();
            if (data == null) {
                data = content.getContentBytes();
            }

            String digestAlgName = digestAlgorithmId.getName();
            algorithms.put(digestAlgorithmId,
                new AlgorithmInfo("SignerInfo digestAlgorithm field", false));

            byte[] dataSigned;

            // if there are authenticated attributes, get the message
            // digest and compare it with the digest of data
            if (authenticatedAttributes == null) {
                dataSigned = data;
            } else {

                // first, check content type
                ObjectIdentifier contentType = (ObjectIdentifier)
                       authenticatedAttributes.getAttributeValue(
                         PKCS9Attribute.CONTENT_TYPE_OID);
                if (contentType == null ||
                    !contentType.equals(content.contentType))
                    return null;  // contentType does not match, bad SignerInfo

                // now, check message digest
                byte[] messageDigest = (byte[])
                    authenticatedAttributes.getAttributeValue(
                         PKCS9Attribute.MESSAGE_DIGEST_OID);

                if (messageDigest == null) // fail if there is no message digest
                    return null;

                byte[] computedMessageDigest;
                if (digestAlgName.equals("SHAKE256")
                        || digestAlgName.equals("SHAKE256-LEN")) {
                    if (digestAlgName.equals("SHAKE256-LEN")) {
                        // RFC8419: for EdDSA in CMS, the id-shake256-len
                        // algorithm id must contain parameter value 512
                        // encoded as a positive integer value
                        byte[] params = digestAlgorithmId.getEncodedParams();
                        if (params == null) {
                            throw new SignatureException(
                                    "id-shake256-len oid missing length");
                        }
                        int v = new DerValue(params).getInteger();
                        if (v != 512) {
                            throw new SignatureException(
                                    "Unsupported id-shake256-" + v);
                        }
                    }
                    var md = new SHAKE256(64);
                    md.update(data, 0, data.length);
                    computedMessageDigest = md.digest();
                } else {
                    MessageDigest md = MessageDigest.getInstance(digestAlgName);
                    computedMessageDigest = md.digest(data);
                }

                if (!MessageDigest.isEqual(messageDigest, computedMessageDigest)) {
                    return null;
                }

                // message digest attribute matched
                // digest of original data

                // the data actually signed is the DER encoding of
                // the authenticated attributes (tagged with
                // the "SET OF" tag, not 0xA0).
                dataSigned = authenticatedAttributes.getDerEncoding();
            }

            // put together digest algorithm and encryption algorithm
            // to form signing algorithm. See makeSigAlg for details.
            String sigAlgName = makeSigAlg(
                    digestAlgorithmId,
                    digestEncryptionAlgorithmId);

            KnownOIDs oid = KnownOIDs.findMatch(sigAlgName);
            if (oid != null) {
                AlgorithmId sigAlgId =
                    new AlgorithmId(ObjectIdentifier.of(oid),
                            digestEncryptionAlgorithmId.getParameters());
                algorithms.put(sigAlgId,
                    new AlgorithmInfo(
                        "SignerInfo digestEncryptionAlgorithm field", true));
            }

            X509Certificate cert = getCertificate(block);
            if (cert == null) {
                return null;
            }
            PublicKey key = cert.getPublicKey();

            if (cert.hasUnsupportedCriticalExtension()) {
                throw new SignatureException("Certificate has unsupported "
                                             + "critical extension(s)");
            }

            algorithmsConformanceCheck(
                    digestAlgorithmId,
                    digestEncryptionAlgorithmId,
                    key,
                    authenticatedAttributes == null);

            // Make sure that if the usage of the key in the certificate is
            // restricted, it can be used for digital signatures.
            // XXX We may want to check for additional extensions in the
            // future.
            boolean[] keyUsageBits = cert.getKeyUsage();
            if (keyUsageBits != null) {
                KeyUsageExtension keyUsage;
                // We don't care whether this extension was marked
                // critical in the certificate.
                // We're interested only in its value (i.e., the bits set)
                // and treat the extension as critical.
                keyUsage = new KeyUsageExtension(keyUsageBits);

                boolean digSigAllowed
                        = keyUsage.get(KeyUsageExtension.DIGITAL_SIGNATURE);

                boolean nonRepuAllowed
                        = keyUsage.get(KeyUsageExtension.NON_REPUDIATION);

                if (!digSigAllowed && !nonRepuAllowed) {
                    throw new SignatureException("Key usage restricted: "
                                                 + "cannot be used for "
                                                 + "digital signatures");
                }
            }

            Signature sig = Signature.getInstance(sigAlgName);

            AlgorithmParameters ap =
                digestEncryptionAlgorithmId.getParameters();
            try {
                SignatureUtil.initVerifyWithParam(sig, key,
                    SignatureUtil.getParamSpec(sigAlgName, ap));
            } catch (ProviderException | InvalidAlgorithmParameterException |
                     InvalidKeyException e) {
                throw new SignatureException(e.getMessage(), e);
            }

            sig.update(dataSigned);
            if (sig.verify(encryptedDigest)) {
                return this;
            }
        } catch (IOException e) {
            throw new SignatureException("Error verifying signature", e);
        }
        return null;
    }

    /**
     * Checks if the digest algorithm and encryption algorithm combination
     * inside a PKCS7 SignerInfo is legal.
     *
     * @param digAlgId the digest algorithm
     * @param encAlgId the encryption algorithm
     * @param key the public key for verification
     * @param directSign whether the signature is calculated on the content
     *                   directly. This makes difference for Ed448.
     */
    private static void algorithmsConformanceCheck(
            AlgorithmId digAlgId, AlgorithmId encAlgId, PublicKey key,
            boolean directSign) throws NoSuchAlgorithmException {
        String encAlg = encAlgId.getName();
        switch (encAlg) {
            case "RSASSA-PSS":
                PSSParameterSpec spec = (PSSParameterSpec)
                        SignatureUtil.getParamSpec(encAlg, encAlgId.getParameters());
                /*
                 * RFC 4056 section 3 for Signed-data:
                 * signatureAlgorithm MUST contain id-RSASSA-PSS. The algorithm
                 * parameters field MUST contain RSASSA-PSS-params.
                 */
                if (spec == null) {
                    throw new NoSuchAlgorithmException("Missing PSSParameterSpec for RSASSA-PSS algorithm");
                }

                if (!AlgorithmId.get(spec.getDigestAlgorithm()).equals(digAlgId)) {
                    throw new NoSuchAlgorithmException("Incompatible digest algorithm");
                }
                break;
            case "Ed25519":
                if (!digAlgId.equals(SignatureUtil.EdDSADigestAlgHolder.sha512)) {
                    throw new NoSuchAlgorithmException("Incompatible digest algorithm");
                }
                break;
            case "Ed448":
                if (directSign) {
                    if (!digAlgId.equals(SignatureUtil.EdDSADigestAlgHolder.shake256)) {
                        throw new NoSuchAlgorithmException("Incompatible digest algorithm");
                    }
                } else {
                    if (!digAlgId.equals(SignatureUtil.EdDSADigestAlgHolder.shake256$512)) {
                        throw new NoSuchAlgorithmException("Incompatible digest algorithm");
                    }
                }
                break;
            case "HSS/LMS":
                // RFC 8708 requires the same hash algorithm used as in the HSS/LMS algorithm
                if (!digAlgId.equals(AlgorithmId.get(KeyUtil.hashAlgFromHSS(key)))) {
                    throw new NoSuchAlgorithmException("Incompatible digest algorithm");
                }
                break;
        }
    }

    /**
     * Derives the signature algorithm name from the digest algorithm
     * and the encryption algorithm inside a PKCS7 SignerInfo.
     *
     * The digest algorithm is in the form "DIG", and the encryption
     * algorithm can be in any of the 3 forms:
     *
     * 1. Old style key algorithm like RSA, DSA, EC, this method returns
     *    DIGwithKEY.
     * 2. New style signature algorithm in the form of HASHwithKEY, this
     *    method returns DIGwithKEY. Please note this is not HASHwithKEY.
     * 3. Modern signature algorithm like RSASSA-PSS and EdDSA, this method
     *    returns the signature algorithm itself.
     *
     * @param digAlgId the digest algorithm
     * @param encAlgId the encryption algorithm
     */
    public static String makeSigAlg(AlgorithmId digAlgId, AlgorithmId encAlgId) {
        String encAlg = encAlgId.getName();
        switch (encAlg) {
            case "RSASSA-PSS":
            case "Ed25519":
            case "Ed448":
            case "HSS/LMS":
                return encAlg;
            default:
                String digAlg = digAlgId.getName();
                String keyAlg = SignatureUtil.extractKeyAlgFromDwithE(encAlg);
                if (keyAlg == null) {
                    // The encAlg used to be only the key alg
                    keyAlg = encAlg;
                }
                if (digAlg.startsWith("SHA-")) {
                    digAlg = "SHA" + digAlg.substring(4);
                }
                if (keyAlg.equals("EC")) keyAlg = "ECDSA";
                String sigAlg = digAlg + "with" + keyAlg;
                try {
                    Signature.getInstance(sigAlg);
                    return sigAlg;
                } catch (NoSuchAlgorithmException e) {
                    // Possibly an unknown modern signature algorithm,
                    // in this case, encAlg should already be a signature
                    // algorithm.
                    return encAlg;
                }
        }
    }

    /* Verify the content of the pkcs7 block. */
    SignerInfo verify(PKCS7 block)
        throws NoSuchAlgorithmException, SignatureException {
        return verify(block, null);
    }

    public BigInteger getVersion() {
            return version;
    }

    public X500Name getIssuerName() {
        return issuerName;
    }

    public BigInteger getCertificateSerialNumber() {
        return certificateSerialNumber;
    }

    public AlgorithmId getDigestAlgorithmId() {
        return digestAlgorithmId;
    }

    public PKCS9Attributes getAuthenticatedAttributes() {
        return authenticatedAttributes;
    }

    public AlgorithmId getDigestEncryptionAlgorithmId() {
        return digestEncryptionAlgorithmId;
    }

    public byte[] getEncryptedDigest() {
        return encryptedDigest;
    }

    public PKCS9Attributes getUnauthenticatedAttributes() {
        return unauthenticatedAttributes;
    }

    /**
     * Returns the timestamp PKCS7 data unverified.
     * @return a PKCS7 object
     */
    public PKCS7 getTsToken() throws IOException {
        if (unauthenticatedAttributes == null) {
            return null;
        }
        PKCS9Attribute tsTokenAttr =
                unauthenticatedAttributes.getAttribute(
                        PKCS9Attribute.SIGNATURE_TIMESTAMP_TOKEN_OID);
        if (tsTokenAttr == null) {
            return null;
        }
        return new PKCS7((byte[])tsTokenAttr.getValue());
    }

    /*
     * Extracts a timestamp from a PKCS7 SignerInfo.
     *
     * Examines the signer's unsigned attributes for a
     * {@code signatureTimestampToken} attribute. If present,
     * then it is parsed to extract the date and time at which the
     * timestamp was generated.
     *
     * @param info A signer information element of a PKCS 7 block.
     *
     * @return A timestamp token or null if none is present.
     * @throws IOException if an error is encountered while parsing the
     *         PKCS7 data.
     * @throws NoSuchAlgorithmException if an error is encountered while
     *         verifying the PKCS7 object.
     * @throws SignatureException if an error is encountered while
     *         verifying the PKCS7 object.
     * @throws CertificateException if an error is encountered while generating
     *         the TSA's certpath.
     */
    public Timestamp getTimestamp()
        throws IOException, NoSuchAlgorithmException, SignatureException,
               CertificateException
    {
        if (timestamp != null || !hasTimestamp)
            return timestamp;

        PKCS7 tsToken = getTsToken();
        if (tsToken == null) {
            hasTimestamp = false;
            return null;
        }

        // Extract the content (an encoded timestamp token info)
        byte[] encTsTokenInfo = tsToken.getContentInfo().getData();
        // Extract the signer (the Timestamping Authority)
        // while verifying the content
        SignerInfo[] tsa = tsToken.verify(encTsTokenInfo);
        if (tsa == null || tsa.length == 0) {
            throw new SignatureException("Unable to verify timestamp");
        }
        // Expect only one signer
        ArrayList<X509Certificate> chain = tsa[0].getCertificateChain(tsToken);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        CertPath tsaChain = cf.generateCertPath(chain);
        // Create a timestamp token info object
        TimestampToken tsTokenInfo = new TimestampToken(encTsTokenInfo);
        // Check that the signature timestamp applies to this signature
        verifyTimestamp(tsTokenInfo);
        algorithms.putAll(tsa[0].algorithms);
        // Create a timestamp object
        timestamp = new Timestamp(tsTokenInfo.getDate(), tsaChain);
        return timestamp;
    }

    /*
     * Check that the signature timestamp applies to this signature.
     * Match the hash present in the signature timestamp token against the hash
     * of this signature.
     */
    private void verifyTimestamp(TimestampToken token)
        throws NoSuchAlgorithmException, SignatureException {

        AlgorithmId digestAlgId = token.getHashAlgorithm();
        algorithms.put(digestAlgId,
            new AlgorithmInfo("TimestampToken digestAlgorithm field", false));

        MessageDigest md = MessageDigest.getInstance(digestAlgId.getName());

        if (!MessageDigest.isEqual(token.getHashedMessage(),
            md.digest(encryptedDigest))) {

            throw new SignatureException("Signature timestamp (#" +
                Debug.toString(token.getSerialNumber()) +
                ") generated on " + token.getDate() + " is inapplicable");
        }

        if (debug != null) {
            debug.println();
            debug.println("Detected signature timestamp (#" +
                Debug.toString(token.getSerialNumber()) +
                ") generated on " + token.getDate());
            debug.println();
        }
    }

    public String toString() {
        HexDumpEncoder hexDump = new HexDumpEncoder();

        String out = "";

        out += "Signer Info for (issuer): " + issuerName + "\n";
        out += "\tversion: " + Debug.toHexString(version) + "\n";
        out += "\tcertificateSerialNumber: " +
               Debug.toHexString(certificateSerialNumber) + "\n";
        out += "\tdigestAlgorithmId: " + digestAlgorithmId + "\n";
        if (authenticatedAttributes != null) {
            out += "\tauthenticatedAttributes: " + authenticatedAttributes +
                   "\n";
        }
        out += "\tdigestEncryptionAlgorithmId: " + digestEncryptionAlgorithmId +
            "\n";

        out += "\tencryptedDigest: " + "\n" +
            hexDump.encodeBuffer(encryptedDigest) + "\n";
        if (unauthenticatedAttributes != null) {
            out += "\tunauthenticatedAttributes: " +
                   unauthenticatedAttributes + "\n";
        }
        return out;
    }

    /**
     * Verify all the algorithms in the array of SignerInfos against the
     * constraints in the jdk.jar.disabledAlgorithms security property.
     *
     * @param infos array of SignerInfos
     * @param params constraint parameters
     * @param name the name of the signer's PKCS7 file
     * @return a set of algorithms that passed the checks and are not disabled
     */
    public static Set<String> verifyAlgorithms(SignerInfo[] infos,
        JarConstraintsParameters params, String name) throws SignatureException {
        Map<AlgorithmId, AlgorithmInfo> algorithms = new HashMap<>();
        for (SignerInfo info : infos) {
            algorithms.putAll(info.algorithms);
        }

        Set<String> enabledAlgorithms = new HashSet<>();
        try {
            for (var algEntry : algorithms.entrySet()) {
                AlgorithmInfo info = algEntry.getValue();
                params.setExtendedExceptionMsg(name, info.field());
                AlgorithmId algId = algEntry.getKey();
                JAR_DISABLED_CHECK.permits(algId.getName(),
                    algId.getParameters(), params, info.checkKey());
                enabledAlgorithms.add(algId.getName());
            }
        } catch (CertPathValidatorException e) {
            throw new SignatureException(e);
        }
        return enabledAlgorithms;
    }
}
