All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.simplify4u.plugins.pgp.SignatureUtils Maven / Gradle / Ivy

/*
 * Copyright 2017-2021 Slawomir Jaranowski
 * Portions Copyright 2017-2018 Wren Security.
 * Portions Copyright 2019 Danny van Heumen
 *
 * 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 org.simplify4u.plugins.pgp;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.util.Optional;

import javax.inject.Named;
import javax.inject.Singleton;

import io.vavr.control.Try;
import org.apache.maven.artifact.Artifact;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.bcpg.sig.IssuerFingerprint;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
import org.simplify4u.plugins.keyserver.PGPKeyNotFound;
import org.simplify4u.plugins.keyserver.PGPKeysCache;
import org.simplify4u.plugins.utils.HexUtils;

/**
 * Utilities for PGP Signature class.
 */
@Named
@Singleton
public class SignatureUtils {

    /**
     * Check PGP signature for bad algorithms.
     *
     * @param hashAlgorithm PGP signature hashAlgorithm
     * @return Returns null if no bad algorithms used, or algorithm name if used.
     */
    public String checkWeakHashAlgorithm(int hashAlgorithm) {
        switch (hashAlgorithm) {
            case HashAlgorithmTags.MD5:
                return "MD5";
            case HashAlgorithmTags.DOUBLE_SHA:
                return "double-width SHA";
            case HashAlgorithmTags.MD2:
                return "MD2";
            case HashAlgorithmTags.TIGER_192:
                return "TIGER/192";
            case HashAlgorithmTags.HAVAL_5_160:
                return "HAVAL (5 pass, 160-bit)";
            case HashAlgorithmTags.SHA224:
                return "SHA-224";
            case HashAlgorithmTags.SHA1:
                // fallthrough
            case HashAlgorithmTags.RIPEMD160:
                // fallthrough
            case HashAlgorithmTags.SHA256:
                // fallthrough
            case HashAlgorithmTags.SHA384:
                // fallthrough
            case HashAlgorithmTags.SHA512:
                return null;
            default:
                throw new UnsupportedOperationException("Unknown hash algorithm value encountered: "
                        + hashAlgorithm);
        }
    }

    /**
     * Load PGPSignature from input stream.
     *
     * @param input the input stream having PGPSignature content
     * @return Returns the (first) read PGP signature.
     * @throws SignatureException In case of failure loading signature.
     */
    public PGPSignature loadSignature(InputStream input) throws SignatureException {

        try {
            InputStream sigInputStream = PGPUtil.getDecoderStream(input);
            PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(sigInputStream, new BcKeyFingerprintCalculator());

            Object nextObject;
            while ((nextObject = pgpObjectFactory.nextObject()) != null) {

                if (nextObject instanceof PGPSignatureList) {
                    return ((PGPSignatureList) nextObject).get(0);
                }

                if (nextObject instanceof PGPCompressedData) {
                    // next read content of compressed message
                    pgpObjectFactory = new PGPObjectFactory(((PGPCompressedData) nextObject).getDataStream(),
                            new BcKeyFingerprintCalculator());
                }

                if (nextObject instanceof PGPLiteralData) {
                    InputStream dataStream = ((PGPLiteralData) nextObject).getDataStream();
                    byte[] buf = new byte[8192];
                    while (dataStream.read(buf) > 0) {
                        // we must read whole packet in order to proper input stream shift
                    }
                }
            }
        } catch (IOException | PGPException e) {
            throw new SignatureException(e.getMessage(), e);
        }

        throw new SignatureException("PGP signature not found.");
    }

    /**
     * Load PGPSignature from file.
     *
     * @param file the file having PGPSignature content
     * @return Returns the (first) read PGP signature.
     * @throws SignatureException In case of failure loading signature.
     * @throws IOException        In case of IO failures.
     */
    public PGPSignature loadSignature(File file) throws IOException, SignatureException {
        try (InputStream in = Files.newInputStream(file.toPath())) {
            return loadSignature(in);
        }
    }

    /**
     * Read the content of a file into the PGP signature instance (for verification).
     *
     * @param signature the PGP signature instance. The instance is expected to be initialized.
     * @param file      the file to read
     * @throws IOException In case of failure to open the file or failure while reading its content.
     */
    public void readFileContentInto(final PGPSignature signature, final File file) throws IOException {
        try (InputStream inArtifact = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
            byte[] buf = new byte[8192];
            int t;
            while ((t = inArtifact.read(buf)) >= 0) {
                signature.update(buf, 0, t);
            }
        }
    }

    /**
     * Retrieve Key Id from signature ISSUER_FINGERPRINT subpackage or standard keyId.
     *
     * @param signature the PGP signature instance
     * @return Returns the keyId from signature
     * @throws SignatureException In case of problem with signature data
     */
    public KeyId retrieveKeyId(PGPSignature signature) throws SignatureException {

        Optional hashedSubPackets = Optional
                .ofNullable(signature.getHashedSubPackets());

        Optional unHashedSubPackets = Optional
                .ofNullable(signature.getUnhashedSubPackets());

        // more of time issuerFingerprint is in hashedSubPackets
        Optional issuerFingerprint = hashedSubPackets
                .map(PGPSignatureSubpacketVector::getIssuerFingerprint);

        if (!issuerFingerprint.isPresent()) {
            issuerFingerprint = unHashedSubPackets.map(PGPSignatureSubpacketVector::getIssuerFingerprint);
        }

        // more of time issuerKeyId is in unHashedSubPackets
        // getIssuerKeyID return 0 (zero) if subpackage not exist
        Optional issuerKeyId = unHashedSubPackets
                .map(PGPSignatureSubpacketVector::getIssuerKeyID)
                .filter(id -> id != 0L);

        if (!issuerKeyId.isPresent()) {
            issuerKeyId = hashedSubPackets
                    .map(PGPSignatureSubpacketVector::getIssuerKeyID)
                    .filter(id -> id != 0L);
        }

        // test issuerKeyId package and keyId form signature
        if (issuerKeyId.isPresent() && signature.getKeyID() != issuerKeyId.get()) {
            throw new SignatureException(
                    String.format("Signature KeyID 0x%016X is not equals to IssuerKeyID 0x%016X",
                            signature.getKeyID(), issuerKeyId.get()));
        }

        // from RFC
        // If the version of the issuing key is 4 and an Issuer subpacket is also included in the signature,
        // the key ID of the Issuer subpacket MUST match the low 64 bits of the fingerprint.
        if (issuerKeyId.isPresent() && issuerFingerprint.isPresent() && issuerFingerprint.get().getKeyVersion() == 4) {
            byte[] bKey = new byte[8];
            byte[] fingerprint = issuerFingerprint.get().getFingerprint();
            System.arraycopy(fingerprint, fingerprint.length - 8, bKey, 0, 8);
            BigInteger bigInteger = new BigInteger(bKey);
            if (bigInteger.longValue() != issuerKeyId.get()) {
                throw new SignatureException(
                        String.format("Signature IssuerFingerprint 0x%s not contains IssuerKeyID 0x%016X",
                                HexUtils.fingerprintToString(fingerprint), issuerKeyId.get()));
            }
        }

        KeyId keyId;
        if (issuerFingerprint.isPresent()) {
            keyId = KeyId.from(issuerFingerprint.get().getFingerprint());
        } else if (issuerKeyId.isPresent()) {
            keyId = KeyId.from(issuerKeyId.get());
        } else {
            keyId = KeyId.from(signature.getKeyID());
        }

        return keyId;
    }

    /**
     * Create {@link SignatureCheckResult} contains data about artifact, signature, public key used to verify and
     * verification status.
     *
     * @param artifact    The artifact to check signature
     * @param artifactAsc The artifact contains signature
     * @param onlyResolve Only resolve signature and keys
     * @param cache       PGP cache for access public key
     * @return check verification result
     */
    private SignatureCheckResult checkSignature(Artifact artifact, Artifact artifactAsc,
                                                boolean onlyResolve, PGPKeysCache cache) {

        SignatureCheckResult.SignatureCheckResultBuilder signatureCheckResultBuilder = SignatureCheckResult.builder();

        signatureCheckResultBuilder.artifact(ArtifactInfo.builder()
                .groupId(artifact.getGroupId())
                .artifactId(artifact.getArtifactId())
                .type(artifact.getType())
                .classifier(artifact.getClassifier())
                .version(artifact.getVersion())
                .build());

        if (!artifact.isResolved()) {
            return signatureCheckResultBuilder.status(SignatureStatus.ARTIFACT_NOT_RESOLVED).build();
        }

        if (artifactAsc == null || !artifactAsc.isResolved()) {
            return signatureCheckResultBuilder.status(SignatureStatus.SIGNATURE_NOT_RESOLVED).build();
        }

        PGPSignature signature = Try.of(() -> loadSignature(artifactAsc.getFile()))
                .onFailure(e ->
                        signatureCheckResultBuilder.errorCause(e).status(SignatureStatus.SIGNATURE_ERROR))
                .getOrNull();

        if (signature == null) {
            return signatureCheckResultBuilder.build();
        }

        KeyId keyId = Try.of(() -> retrieveKeyId(signature))
                .onFailure(e ->
                        signatureCheckResultBuilder.errorCause(e).status(SignatureStatus.SIGNATURE_ERROR))
                .getOrNull();

        if (keyId == null) {
            return signatureCheckResultBuilder.build();
        }

        signatureCheckResultBuilder.signature(
                SignatureInfo.builder()
                        .hashAlgorithm(signature.getHashAlgorithm())
                        .keyAlgorithm(signature.getKeyAlgorithm())
                        .date(signature.getCreationTime())
                        .keyId(keyId)
                        .version(signature.getVersion())
                        .build());

        PGPPublicKeyRing publicKeys = Try.of(() -> cache.getKeyRing(keyId))
                .onFailure(e -> signatureCheckResultBuilder.errorCause(e).status(SignatureStatus.ERROR))
                .onFailure(PGPKeyNotFound.class, e -> signatureCheckResultBuilder.status(SignatureStatus.KEY_NOT_FOUND))
                .getOrNull();

        signatureCheckResultBuilder.keyShowUrl(cache.getUrlForShowKey(keyId));

        if (publicKeys == null) {
            return signatureCheckResultBuilder.build();
        }

        PGPPublicKey publicKey = keyId.getKeyFromRing(publicKeys);

        signatureCheckResultBuilder.key(KeyInfo.builder()
                .fingerprint(new KeyFingerprint(publicKey.getFingerprint()))
                .master(PublicKeyUtils.getMasterKey(publicKey, publicKeys)
                        .map(PGPPublicKey::getFingerprint)
                        .map(KeyFingerprint::new)
                        .orElse(null))
                .uids(PublicKeyUtils.getUserIDs(publicKey, publicKeys))
                .version(publicKey.getVersion())
                .algorithm(publicKey.getAlgorithm())
                .bits(publicKey.getBitStrength())
                .date(publicKey.getCreationTime())
                .build());

        if (onlyResolve) {
            return signatureCheckResultBuilder.status(SignatureStatus.RESOLVED).build();
        }

        Boolean verifyStatus = Try.of(() -> {
                    signature.init(new BcPGPContentVerifierBuilderProvider(), publicKey);
                    readFileContentInto(signature, artifact.getFile());
                    return signature.verify();
                }).onFailure(e -> signatureCheckResultBuilder.errorCause(e).status(SignatureStatus.ERROR))
                .getOrNull();

        if (verifyStatus == null) {
            return signatureCheckResultBuilder.build();
        }

        return signatureCheckResultBuilder
                .status(Boolean.TRUE.equals(verifyStatus)
                        ? SignatureStatus.SIGNATURE_VALID : SignatureStatus.SIGNATURE_INVALID)
                .build();
    }

    /**
     * Create {@link SignatureCheckResult} contains data about artifact, signature, public key used to verify and
     * verification status.
     *
     * @param artifact    The artifact to check signature
     * @param artifactAsc The artifact contains signature
     * @param cache       PGP cache for access public key
     * @return check verification result
     */
    public SignatureCheckResult checkSignature(Artifact artifact, Artifact artifactAsc, PGPKeysCache cache) {
        return checkSignature(artifact, artifactAsc, false, cache);
    }

    /**
     * Only resolve signatures and keys - no validation.
     * Create {@link SignatureCheckResult} contains data about artifact, signature, public key used to verify.
     *
     * @param artifact    The artifact to check signature
     * @param artifactAsc The artifact contains signature
     * @param cache       PGP cache for access public key
     * @return check verification result
     */
    public SignatureCheckResult resolveSignature(Artifact artifact, Artifact artifactAsc, PGPKeysCache cache) {
        return checkSignature(artifact, artifactAsc, true, cache);
    }

    /**
     * Map Public-Key algorithms id to name
     *
     * @param keyAlgorithm key algorithm id
     * @return key algorithm name
     * @throws UnsupportedOperationException if algorithm is is not known
     */
    @SuppressWarnings("deprecation")
    public String keyAlgorithmName(int keyAlgorithm) {
        switch (keyAlgorithm) {
            case PublicKeyAlgorithmTags.RSA_GENERAL:
                return "RSA (Encrypt or Sign)";
            case PublicKeyAlgorithmTags.RSA_ENCRYPT:
                return "RSA Encrypt-Only";
            case PublicKeyAlgorithmTags.RSA_SIGN:
                return "RSA Sign-Only";
            case PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT:
                return "Elgamal (Encrypt-Only)";
            case PublicKeyAlgorithmTags.DSA:
                return "DSA (Digital Signature Algorithm)";
            case PublicKeyAlgorithmTags.ECDH:
                return "Elliptic Curve";
            case PublicKeyAlgorithmTags.ECDSA:
                return "Elliptic Curve Digital Signature";
            case PublicKeyAlgorithmTags.ELGAMAL_GENERAL:
                return "Elgamal (Encrypt or Sign)";
            case PublicKeyAlgorithmTags.DIFFIE_HELLMAN:
                return "Diffie-Hellman";
            case PublicKeyAlgorithmTags.EDDSA:
                return "EdDSA";
            case PublicKeyAlgorithmTags.X25519:
                return "X25519";
            case PublicKeyAlgorithmTags.X448:
                return "X448";
            case PublicKeyAlgorithmTags.Ed25519:
                return "Ed25519";
            case PublicKeyAlgorithmTags.Ed448:
                return "Ed448";
            case PublicKeyAlgorithmTags.EXPERIMENTAL_1:
            case PublicKeyAlgorithmTags.EXPERIMENTAL_2:
            case PublicKeyAlgorithmTags.EXPERIMENTAL_3:
            case PublicKeyAlgorithmTags.EXPERIMENTAL_4:
            case PublicKeyAlgorithmTags.EXPERIMENTAL_5:
            case PublicKeyAlgorithmTags.EXPERIMENTAL_6:
            case PublicKeyAlgorithmTags.EXPERIMENTAL_7:
            case PublicKeyAlgorithmTags.EXPERIMENTAL_8:
            case PublicKeyAlgorithmTags.EXPERIMENTAL_9:
            case PublicKeyAlgorithmTags.EXPERIMENTAL_10:
            case PublicKeyAlgorithmTags.EXPERIMENTAL_11:
                return "Experimental - " + keyAlgorithm;
            default:
                throw new UnsupportedOperationException("Unknown key algorithm value encountered: " + keyAlgorithm);
        }
    }

    /**
     * Map a hashAlgorithm into name.
     *
     * @param hashAlgorithm a hashAlgorithm
     * @return a name of hashAlgorithm
     */
    public String digestName(int hashAlgorithm) {
        switch (hashAlgorithm) {
            case HashAlgorithmTags.SHA1:
                return "SHA1";
            case HashAlgorithmTags.DOUBLE_SHA:
                return "double-width SHA";
            case HashAlgorithmTags.MD2:
                return "MD2";
            case HashAlgorithmTags.MD4:
                return "MD4";
            case HashAlgorithmTags.MD5:
                return "MD5";
            case HashAlgorithmTags.HAVAL_5_160:
                return "HAVAL (5 pass, 160-bit)";
            case HashAlgorithmTags.RIPEMD160:
                return "RIPEMD160";
            case HashAlgorithmTags.SHA256:
                return "SHA256";
            case HashAlgorithmTags.SHA384:
                return "SHA384";
            case HashAlgorithmTags.SHA512:
                return "SHA512";
            case HashAlgorithmTags.SHA224:
                return "SHA224";
            case HashAlgorithmTags.SHA3_256:
            case HashAlgorithmTags.SHA3_256_OLD:
                return "SHA256";
            case HashAlgorithmTags.SHA3_384:
                return "SHA384";
            case HashAlgorithmTags.SHA3_512:
            case HashAlgorithmTags.SHA3_512_OLD:
                return "SHA512";
            case HashAlgorithmTags.SHA3_224:
                return "SHA224";
            case HashAlgorithmTags.TIGER_192:
                return "TIGER";
            case HashAlgorithmTags.SM3:
                return "SM3";
            default:
                throw new UnsupportedOperationException(
                        "Unknown hash algorithm tag in digestName: " + hashAlgorithm);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy