Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PKCS#7 CMS signing and verification #526

Merged
merged 70 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
ca39bf6
Initial buttons and menu items for verify signature action.
jonwltn Nov 13, 2024
c581926
Moved appbundler dependency spec to better location.
jonwltn Nov 13, 2024
74adc62
Added verify signature file chooser.
jonwltn Nov 14, 2024
c299030
Added basic signed data/signature verification.
jonwltn Nov 15, 2024
46b49c7
Hooked up view signature dialog showing only the issuer DN.
jonwltn Nov 17, 2024
4280243
Initial population of the form content.
jonwltn Nov 18, 2024
e163bb9
Initial update to resource files for DViewSignature. Added content ty…
jonwltn Nov 18, 2024
eb73723
Cleaned up view signature dialog and resources. Displayed subject name.
jonwltn Nov 19, 2024
ce1f382
Added view certificates button.
jonwltn Nov 19, 2024
806d0f6
Added counter signers button.
jonwltn Nov 21, 2024
0499a55
Fixed warning about PEMParser not being closed.
jonwltn Nov 21, 2024
ec9b50f
Fixed potential NPE when a signer is not selected.
jonwltn Nov 21, 2024
8c7a12b
Populated signature algorithm. Verified the signature.
jonwltn Nov 24, 2024
2694b65
Signed attribute table can be null.
jonwltn Nov 25, 2024
3bc633a
Handle PEM encoded signatures.
jonwltn Nov 25, 2024
85273e2
Hooked up the PEM button.
jonwltn Nov 25, 2024
4c3e444
Hooked up the ANS1 button.
jonwltn Nov 25, 2024
b216d6e
Refactoring and cleanup.
jonwltn Nov 27, 2024
90fadb2
Removed embedded CertHolder to X509Certificate conversion loigc.
jonwltn Nov 27, 2024
2a92e6c
Work with both attached and detached signatures.
jonwltn Nov 30, 2024
c63a671
Added sign file menu item and dialogs.
jonwltn Dec 2, 2024
b6e5e9a
Hooked up the digital signature code.
jonwltn Dec 3, 2024
f01e9d4
Added time stamping and viewing of the time stamp signer.
jonwltn Dec 6, 2024
cdfbfda
Added counter signing -- not fully complete (see TODOs).
jonwltn Dec 9, 2024
9413e1b
Added file chooser for detached content for counter signing.
jonwltn Dec 10, 2024
3322d04
Clean up. Added icon files for signing. Enabled PEM output.
jonwltn Dec 10, 2024
7e984c3
Remove signature output PEM encoding TODO.
jonwltn Dec 10, 2024
83e7a3c
Fixed RSAPSS algorithm lookup TODO.
jonwltn Dec 10, 2024
ccbbf79
Fixed DSignFile layout.
jonwltn Dec 10, 2024
28545d4
Fixed TODO for CmsSigner exception. Parameterized DigestType for time…
jonwltn Dec 10, 2024
d33b109
Fixed some warnings.
jonwltn Dec 10, 2024
67283a6
Fixed more warnings and a TODO.
jonwltn Dec 10, 2024
c593166
Added support for an email address to SignerListCellRend.
jonwltn Dec 11, 2024
3bf2ce2
Removed cert import pop-up dialog. Hid fields not hooked up in DViewS…
jonwltn Dec 11, 2024
d8e8f7f
Re-enabled signature verification. Update output file based on detach…
jonwltn Dec 12, 2024
4fd282c
Rearranged the buttons on DViewSignature.
jonwltn Dec 12, 2024
e5b6733
Verify counter signers.
jonwltn Dec 12, 2024
3e17c6a
Implement time stamps for counter signers.
jonwltn Dec 12, 2024
4bb4944
Added context to the view Time Stamp and Counter Signers dialogs.
jonwltn Dec 12, 2024
075a52e
Formatted CmsUtil.
jonwltn Dec 12, 2024
82b1595
Restructured signature loading flows. Created TODOs for invalid condi…
jonwltn Dec 13, 2024
2570749
Fixed compile error in DViewPem.
jonwltn Dec 13, 2024
dfd20c2
Added messages for missing content and no signatures.
jonwltn Dec 13, 2024
025e668
Fixed outputFile TODO. Added more context for ASN.1 TODO.
jonwltn Dec 13, 2024
0e8e60a
Updated DSignFile for counter signing. Merged counter signing into Si…
jonwltn Dec 17, 2024
5947caa
Wrapped up DSignDialog.
jonwltn Dec 18, 2024
ca3f38f
Resolved some TODOs in CmsUtil.
jonwltn Dec 18, 2024
775d9ea
Refactoring for signer information management.
jonwltn Dec 19, 2024
3326abe
Revert scope change to X509CertUtil.extractCertificates
jonwltn Dec 19, 2024
de76dcb
Removed a couple more TODOs.
jonwltn Dec 19, 2024
6458262
Fixed TODO in CmsUtil.
jonwltn Dec 20, 2024
33a6ca2
DViewSignature improvements:
jonwltn Dec 20, 2024
74105fb
Display signature status.
jonwltn Dec 23, 2024
90bc87b
Show content type.
jonwltn Dec 24, 2024
8b86856
Moved signatures to a JTree. Counter signatures are children of signa…
jonwltn Dec 24, 2024
765eaa0
Removed inadvertently added file.
jonwltn Dec 24, 2024
80ccb7e
Cleaned up warnings. Fixed selection of first tree node.
jonwltn Dec 25, 2024
aaeff1d
Added TODO about supported digest types for time stamp servers.
jonwltn Dec 25, 2024
8666cdc
Remove content type resource strings. Updated TODOs.T
jonwltn Dec 25, 2024
2b33253
Removed unusd resource strings.
jonwltn Dec 26, 2024
7cbd87a
Removed more unused resource strings. Resolved some TODOs. Fixed TSA …
jonwltn Dec 28, 2024
0189dfc
Use the explicit provider if one is present for signing.
jonwltn Dec 28, 2024
7b26dc8
Resolved more TODOs. Addressed comments from the PR.
jonwltn Jan 1, 2025
cb684ce
Removed feature idea TODOs. Removed unused parameters in DSignFile.
jonwltn Jan 3, 2025
801e5df
Fixed TODOs in DViewSignature.
jonwltn Jan 7, 2025
508bb70
Verify cert trust during signature verification.
jonwltn Jan 7, 2025
38700bd
Use cacerts for verify timestamp cert trust.
jonwltn Jan 7, 2025
c72b9dc
Resolved some more TODOs. Displayed "Not Verified" when the signed co…
jonwltn Jan 10, 2025
3dc3c8e
Removed last remaining TODOs in VerifySignatureAction.
jonwltn Jan 10, 2025
83589ec
Merge branch 'verify-tmp' into verify
jonwltn Jan 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions kse/src/main/java/org/kse/crypto/signing/CmsSignatureStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2004 - 2013 Wayne Grant
* 2013 - 2024 Kai Kramer
*
* This file is part of KeyStore Explorer.
*
* KeyStore Explorer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeyStore Explorer 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeyStore Explorer. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kse.crypto.signing;

import java.util.ResourceBundle;

/**
* Enumeration of signature statuses for verifying a CMS signature.
*/
public enum CmsSignatureStatus {

// @formatter:off

NOT_VERIFIED,
INVALID,
VALID_NOT_TRUSTED,
VALID_TRUSTED;

// @formatter:on

private static ResourceBundle res = ResourceBundle.getBundle("org/kse/crypto/signing/resources");

public String getText() {
return res.getString("CmsSignatureStatus." + name() + ".text");
}

public String getToolTip() {
return res.getString("CmsSignatureStatus." + name() + ".tooltip");
}

}
202 changes: 202 additions & 0 deletions kse/src/main/java/org/kse/crypto/signing/CmsSigner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Copyright 2004 - 2013 Wayne Grant
* 2013 - 2024 Kai Kramer
*
* This file is part of KeyStore Explorer.
*
* KeyStore Explorer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeyStore Explorer 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeyStore Explorer. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kse.crypto.signing;

import java.io.File;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.ResourceBundle;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableFile;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.kse.crypto.CryptoException;
import org.kse.crypto.digest.DigestType;

/**
* Class provides functionality to sign files using PKCS #7 Cryptographic
* Message Syntax (CMS).
*/
public class CmsSigner {
private static ResourceBundle res = ResourceBundle.getBundle("org/kse/crypto/signing/resources");

private CmsSigner() {
}

/**
* Signs a file using PKCS #7 CMS.
*
* @param inputFile The file to sign.
* @param privateKey The private key to use for signing.
* @param certificateChain The certificate chain for the private key.
* @param detachedSignature True if the signature is to be detached. False,
* encapsulate the file into the signature.
* @param signatureType The signature type to use for signing.
* @param tsaUrl An optional TSA URL for adding a time stamp token to
* the signature.
* @param provider
* @return The signature in a CMSSignedData object.
*/
public static CMSSignedData sign(File inputFile, PrivateKey privateKey, X509Certificate[] certificateChain,
boolean detachedSignature, SignatureType signatureType, String tsaUrl, Provider provider)
throws CryptoException {
try {
CMSTypedData msg = new CMSProcessableFile(inputFile);

JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(signatureType.jce());
JcaDigestCalculatorProviderBuilder digestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder();
if (provider != null) {
contentSignerBuilder.setProvider(provider);
digestCalculatorProviderBuilder.setProvider(provider);
}

CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
generator.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(digestCalculatorProviderBuilder.build())
.build(contentSignerBuilder.build(privateKey), certificateChain[0]));
generator.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));

CMSSignedData signedData = generator.generate(msg, !detachedSignature);

if (tsaUrl != null) {
SignerInformationStore signerInfos = addTimestamp(tsaUrl, signedData.getSignerInfos(),
signatureType.digestType());
signedData = CMSSignedData.replaceSigners(signedData, signerInfos);
}

return signedData;
} catch (Exception e) {
throw new CryptoException(res.getString("CmsSignatureFailed.exception.message"), e);
}
}

/**
* Counter signs a signature using PKCS #7 CMS.
*
* @param signedData The signature to counter sign.
* @param privateKey The private key to use for signing.
* @param certificateChain The certificate chain for the private key.
* @param detachedSignature True if the signature is to be detached. False,
* encapsulate the file into the signature.
* @param signatureType The signature type to use for signing.
* @param tsaUrl An optional TSA URL for adding a time stamp token to
* the signature.
* @param provider
* @return The counter signed signature in a CMSSignedData object.
*/
public static CMSSignedData counterSign(CMSSignedData signedData, PrivateKey privateKey,
X509Certificate[] certificateChain, boolean detachedSignature, SignatureType signatureType, String tsaUrl,
Provider provider) throws CryptoException {
try {
JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(signatureType.jce());
JcaDigestCalculatorProviderBuilder digestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder();
if (provider != null) {
contentSignerBuilder.setProvider(provider);
digestCalculatorProviderBuilder.setProvider(provider);
}

CMSSignedDataGenerator counterSignerGen = new CMSSignedDataGenerator();
counterSignerGen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(digestCalculatorProviderBuilder.build())
.build(contentSignerBuilder.build(privateKey), certificateChain[0]));

// Counter signs all existing signatures.
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
for (SignerInformation signer : signedData.getSignerInfos()) {
SignerInformationStore counterSigners = counterSignerGen.generateCounterSigners(signer);

if (tsaUrl != null) {
counterSigners = addTimestamp(tsaUrl, counterSigners, signatureType.digestType());
}

// addCounterSigners does not replace existing counter signers. It creates a new
// counter signer vector if it does not already exist, and then it adds the counter signer.
signer = SignerInformation.addCounterSigners(signer, counterSigners);

generator.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
generator.addSigners(new SignerInformationStore(signer));
}
generator.addCertificates(signedData.getCertificates());

CMSSignedData counterSignedData = generator.generate(signedData.getSignedContent(), !detachedSignature);

return counterSignedData;
} catch (CertificateEncodingException | OperatorCreationException | CMSException | IOException e) {
throw new CryptoException(res.getString("CmsCounterSignatureFailed.exception.message"), e);
}
}

/**
* Adds a timestamp to a PKCS #7 signature.
*
* @param tsaUrl The URL of the time stamp authority.
* @param signerInfos The signer information to time stamp.
* @param digestType The digest type to use for the time stamp.
* @return <b>SignerInformation</b> with time stamp token.
*/
public static SignerInformationStore addTimestamp(String tsaUrl, SignerInformationStore signerInfos,
DigestType digestType) throws IOException {

Collection<SignerInformation> newSignerInfos = new ArrayList<>();

for (SignerInformation si : signerInfos.getSigners()) {
byte[] signature = si.getSignature();

// Ed448 uses digest type of SHAKE256-512, which is not currently supported by the TSAs.
if (DigestType.SHAKE256 == digestType) {
digestType = DigestType.SHA512;
}

// send request to TSA
byte[] token = TimeStampingClient.getTimeStampToken(tsaUrl, signature, digestType);

// create new SignerInformation with TS attribute
Attribute tokenAttr = new Attribute(PKCSObjectIdentifiers.id_aa_signatureTimeStampToken,
new DERSet(ASN1Primitive.fromByteArray(token)));
ASN1EncodableVector timestampVector = new ASN1EncodableVector();
timestampVector.add(tokenAttr);
AttributeTable at = new AttributeTable(timestampVector);

newSignerInfos.add(SignerInformation.replaceUnsignedAttributes(si, at));
}

return new SignerInformationStore(newSignerInfos);
}
}
143 changes: 143 additions & 0 deletions kse/src/main/java/org/kse/crypto/signing/CmsUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright 2004 - 2013 Wayne Grant
* 2013 - 2024 Kai Kramer
*
* This file is part of KeyStore Explorer.
*
* KeyStore Explorer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeyStore Explorer 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeyStore Explorer. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kse.crypto.signing;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.ResourceBundle;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableFile;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.util.Store;
import org.kse.crypto.CryptoException;
import org.kse.utilities.pem.PemInfo;
import org.kse.utilities.pem.PemUtil;

/**
* Provides utility methods relating to Cryptographic Message Syntax (CMS).
*/
public class CmsUtil {
private static ResourceBundle res = ResourceBundle.getBundle("org/kse/crypto/signing/resources");

private static final String CMS_PEM_TYPE = "CMS";
private static final String PKCS7_PEM_TYPE = "PKCS7";

private CmsUtil() {
}

/**
* Loads a signature. If it is a detached signature, attempts to find and load the content.
* Verification and counter signing require the content.
*
* @param signatureFile The signature file.
* @param chooser The file chooser to use for choosing the content file.
* @return
* @throws CryptoException
*/
public static CMSSignedData loadSignature(File signatureFile, Supplier<File> chooser)
throws CryptoException {

try {
byte[] signature = Files.readAllBytes(signatureFile.toPath());

if (PemUtil.isPemFormat(signature)) {
PemInfo signaturePem = PemUtil.decode(signature);
if (signaturePem != null) {
signature = signaturePem.getContent();
}
}

CMSSignedData signedData = new CMSSignedData(signature);

if (signedData.isDetachedSignature()) {
CMSProcessableFile content = loadDetachedContent(signatureFile, chooser);
if (content != null) {
signedData = new CMSSignedData(content, signature);
}
}

return signedData;
} catch (IOException | CMSException e) {
throw new CryptoException(res.getString("NoReadCms.exception.message"), e);
}
}

private static CMSProcessableFile loadDetachedContent(File signatureFile, Supplier<File> chooser) {

// Look for the content file. if not present, prompt for it.
File contentFile = null;

// First try file name with signature extension stripped.
int extensionIndex = signatureFile.getAbsolutePath().lastIndexOf('.');
if (extensionIndex > 0) {
// Turn file_name.txt.p7s into file_name.txt
String contentFileName = signatureFile.getAbsolutePath().substring(0, extensionIndex);
if (Files.exists(Paths.get(contentFileName))) {
contentFile = new File(contentFileName);
}
}

// No file - ask for one (if chooser is available)
if (contentFile == null && chooser != null) {
contentFile = chooser.get();
if (contentFile == null) {
return null;
}
}

return new CMSProcessableFile(contentFile);
}

public static boolean isCmsPemType(PemInfo pemInfo) {
return pemInfo != null && (PKCS7_PEM_TYPE.equals(pemInfo.getType()) || CMS_PEM_TYPE.equals(pemInfo.getType()));
}

/**
* PEM encode a CMS signature.
*
* @param cms The CMS signature
* @return The PEM'd encoding
*/
public static String getPem(CMSSignedData cms) throws CryptoException {
try {
// Use PKCS7 PEM header since it can be verified using GnuTLS certtool and OpenSSL.
// GnuTLS certtool will not verify signatures with the CMS PEM header.
PemInfo pemInfo = new PemInfo(PKCS7_PEM_TYPE, null, cms.getEncoded());
return PemUtil.encode(pemInfo);
} catch (IOException e) {
throw new CryptoException(res.getString("CmsGetPemFailed.exception.message"), e);
}
}

public static List<KseSignerInformation> convertSignerInformations(Collection<SignerInformation> signerInfos,
Store<X509CertificateHolder> trustedCerts, Store<X509CertificateHolder> signatureCerts) {
return signerInfos.stream().map(s -> new KseSignerInformation(s, trustedCerts, signatureCerts))
.collect(Collectors.toList());
}
}
Loading
Loading