Skip to content

Commit

Permalink
Add the commitment type indication and the signing certificate v2 att…
Browse files Browse the repository at this point in the history
…ributes to NuGet signatures (#234)
  • Loading branch information
ebourg committed Jul 23, 2024
1 parent fb951f4 commit de54040
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 9 deletions.
22 changes: 14 additions & 8 deletions jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ public void sign(Signable file) throws Exception {
protected CMSSignedData createSignedData(Signable file) throws Exception {
// compute the signature
CMSTypedData contentInfo = file.createSignedContent(digestAlgorithm);
CMSSignedDataGenerator generator = createSignedDataGenerator(contentInfo);
CMSSignedDataGenerator generator = createSignedDataGenerator(file, contentInfo);
CMSSignedData sigData = generator.generate(contentInfo, true);

// verify the signature
Expand Down Expand Up @@ -419,19 +419,19 @@ protected CMSSignedData createSignedData(Signable file) throws Exception {
return sigData;
}

private CMSSignedDataGenerator createSignedDataGenerator(CMSTypedData contentInfo) throws CMSException, OperatorCreationException, CertificateEncodingException {
private CMSSignedDataGenerator createSignedDataGenerator(Signable file, CMSTypedData contentInfo) throws CMSException, OperatorCreationException, CertificateEncodingException {
List<X509Certificate> fullChain = CertificateUtils.getFullCertificateChain((Collection) Arrays.asList(chain));
fullChain.removeIf(CertificateUtils::isSelfSigned);

boolean authenticode = AuthenticodeObjectIdentifiers.isAuthenticode(contentInfo.getContentType().getId());
CMSSignedDataGenerator generator = authenticode ? new AuthenticodeSignedDataGenerator() : new CMSSignedDataGenerator();
generator.addCertificates(new JcaCertStore(fullChain));
generator.addSignerInfoGenerator(createSignerInfoGenerator());
generator.addSignerInfoGenerator(createSignerInfoGenerator(file, authenticode));

return generator;
}

private SignerInfoGenerator createSignerInfoGenerator() throws OperatorCreationException, CertificateEncodingException {
private SignerInfoGenerator createSignerInfoGenerator(Signable file, boolean authenticode) throws OperatorCreationException, CertificateEncodingException {
// create content signer
JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(getSignatureAlgorithm());
if (signatureProvider != null) {
Expand All @@ -442,8 +442,14 @@ private SignerInfoGenerator createSignerInfoGenerator() throws OperatorCreationE
DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build();

// prepare the authenticated attributes
CMSAttributeTableGenerator attributeTableGenerator = new DefaultSignedAttributeTableGenerator(createAuthenticatedAttributes());
attributeTableGenerator = new FilteredAttributeTableGenerator(attributeTableGenerator, CMSAttributes.signingTime, CMSAttributes.cmsAlgorithmProtect);
List<Attribute> attributes = new ArrayList<>(authenticode ? createAuthenticatedAttributes() : file.createSignedAttributes((X509Certificate) chain[0]));
AttributeTable attributeTable = new AttributeTable(new DERSet(attributes.toArray(new ASN1Encodable[0])));
CMSAttributeTableGenerator attributeTableGenerator = new DefaultSignedAttributeTableGenerator(attributeTable);
if (authenticode) {
attributeTableGenerator = new FilteredAttributeTableGenerator(attributeTableGenerator, CMSAttributes.cmsAlgorithmProtect, CMSAttributes.signingTime);
} else {
attributeTableGenerator = new FilteredAttributeTableGenerator(attributeTableGenerator, CMSAttributes.cmsAlgorithmProtect);
}

// fetch the signing certificate
X509CertificateHolder certificate = new JcaX509CertificateHolder((X509Certificate) chain[0]);
Expand Down Expand Up @@ -501,7 +507,7 @@ private void verify(CMSSignedData signedData) throws SignatureException, Operato
*
* @return the authenticated attributes
*/
private AttributeTable createAuthenticatedAttributes() {
private List<Attribute> createAuthenticatedAttributes() {
List<Attribute> attributes = new ArrayList<>();

SpcStatementType spcStatementType = new SpcStatementType(AuthenticodeObjectIdentifiers.SPC_INDIVIDUAL_SP_KEY_PURPOSE_OBJID);
Expand All @@ -512,7 +518,7 @@ private AttributeTable createAuthenticatedAttributes() {
attributes.add(new Attribute(AuthenticodeObjectIdentifiers.SPC_SP_OPUS_INFO_OBJID, new DERSet(spcSpOpusInfo)));
}

return new AttributeTable(new DERSet(attributes.toArray(new ASN1Encodable[0])));
return attributes;
}

/**
Expand Down
14 changes: 14 additions & 0 deletions jsign-core/src/main/java/net/jsign/Signable.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.ContentInfo;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSTypedData;
Expand Down Expand Up @@ -99,6 +103,16 @@ default byte[] computeDigest(DigestAlgorithm digestAlgorithm) throws IOException
*/
ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException;

/**
* Creates the signed attributes to include in the signature.
*
* @param certificate the signing certificate
* @since 7.0
*/
default List<Attribute> createSignedAttributes(X509Certificate certificate) throws CertificateEncodingException {
return new ArrayList<>();
}

/**
* Checks if the specified certificate is suitable for signing the file.
*
Expand Down
29 changes: 29 additions & 0 deletions jsign-core/src/main/java/net/jsign/nuget/NugetFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,23 @@
import java.io.InputStream;
import java.nio.channels.SeekableByteChannel;
import java.security.MessageDigest;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;

import org.apache.poi.util.IOUtils;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.esf.CommitmentTypeIndication;
import org.bouncycastle.asn1.ess.ESSCertIDv2;
import org.bouncycastle.asn1.ess.SigningCertificateV2;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.IssuerSerial;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSTypedData;
Expand All @@ -44,6 +54,7 @@
* A NuGet package.
*
* @see <a href="https://github.com/NuGet/Home/wiki/Package-Signatures-Technical-Details">NuGet Package Signatures Technical Specification</a>
* @see <a href="https://github.com/NuGet/Home/wiki/Repository-Signatures-and-Countersignatures-Technical-Specification">NuGet Repository Signatures and Countersignatures Technical Specification</a>
*
* @author Sebastian Stamm
* @since 7.0
Expand Down Expand Up @@ -117,6 +128,24 @@ public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) {
throw new UnsupportedOperationException(); // not applicable here
}

@Override
public List<Attribute> createSignedAttributes(X509Certificate certificate) throws CertificateEncodingException {
List<Attribute> attributes = new ArrayList<>();

CommitmentTypeIndication commitmentTypeIndication = new CommitmentTypeIndication(PKCSObjectIdentifiers.id_cti_ets_proofOfOrigin);
attributes.add(new Attribute(PKCSObjectIdentifiers.id_aa_ets_commitmentType, new DERSet(commitmentTypeIndication)));
// todo use the id-cti-ets-proofOfReceipt type for repository signatures

// todo add the nuget-v3-service-index-url and nuget-package-owners attributes for repository signatures

byte[] certHash = DigestAlgorithm.SHA256.getMessageDigest().digest(certificate.getEncoded());
IssuerSerial issuerSerial = new IssuerSerial(X500Name.getInstance(certificate.getIssuerX500Principal().getEncoded()), certificate.getSerialNumber());
SigningCertificateV2 signingCertificateV2 = new SigningCertificateV2(new ESSCertIDv2(certHash, issuerSerial));
attributes.add(new Attribute(PKCSObjectIdentifiers.id_aa_signingCertificateV2, new DERSet(signingCertificateV2)));

return attributes;
}

@Override
public List<CMSSignedData> getSignatures() throws IOException {
if (centralDirectory.entries.containsKey(SIGNATURE_ENTRY)) {
Expand Down
7 changes: 7 additions & 0 deletions jsign-core/src/test/java/net/jsign/NugetSignerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
import org.apache.commons.io.FileUtils;
import org.bouncycastle.asn1.cms.CMSAttributes;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.junit.Test;

import net.jsign.nuget.NugetFile;
Expand Down Expand Up @@ -51,6 +53,11 @@ public void testSign() throws Exception {
signer.sign(file);

SignatureAssert.assertSigned(file, SHA256);

// verify the signed attributes
SignatureAssert.assertSignedAttribute("commitment type indication", file, PKCSObjectIdentifiers.id_aa_ets_commitmentType);
SignatureAssert.assertSignedAttribute("signing certificate v2", file, PKCSObjectIdentifiers.id_aa_signingCertificateV2);
SignatureAssert.assertSignedAttribute("signing time", file, CMSAttributes.signingTime);
}
}

Expand Down
15 changes: 14 additions & 1 deletion jsign-core/src/test/java/net/jsign/SignatureAssert.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.io.IOException;
import java.util.List;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DEROctetString;
Expand Down Expand Up @@ -90,7 +91,9 @@ public static void assertSigned(Signable signable, DigestAlgorithm... algorithms
assertEquals("Digest algorithm of signature " + i, algorithms[i].oid, si.getDigestAlgorithmID().getAlgorithm());

// Check if the signingTime attribute is present
assertNull("signingTime attribute found in signature " + i, signature.getSignerInfos().iterator().next().getSignedAttributes().get(CMSAttributes.signingTime));
if (isAuthenticode(signature.getSignedContentTypeOID())) {
assertNull("signingTime attribute found in signature " + i, signature.getSignerInfos().iterator().next().getSignedAttributes().get(CMSAttributes.signingTime));
}
}
}

Expand All @@ -108,4 +111,14 @@ public static void assertUuidEquals(Signable signable, String expected) throws I

assertEquals("Authenticode UUID", expected.toUpperCase().replaceAll("-", ""), Hex.toHexString(uuid.getOctets()).toUpperCase());
}

public static void assertSignedAttribute(String message, Signable signable, ASN1ObjectIdentifier oid) throws IOException {
SignerInformation signerInformation = signable.getSignatures().get(0).getSignerInfos().getSigners().iterator().next();

AttributeTable attributes = signerInformation.getSignedAttributes();
assertNotNull(message + " (missing signed attributes)", attributes);

Attribute attribute = attributes.get(oid);
assertNotNull(message + " (missing " + oid + " attribute)", attribute);
}
}

0 comments on commit de54040

Please sign in to comment.