From de54040452141d321e2bc2e7d1a30a6d869975ba Mon Sep 17 00:00:00 2001 From: Emmanuel Bourg Date: Tue, 23 Jul 2024 19:11:20 +0200 Subject: [PATCH] Add the commitment type indication and the signing certificate v2 attributes to NuGet signatures (#234) --- .../java/net/jsign/AuthenticodeSigner.java | 22 +++++++++----- .../src/main/java/net/jsign/Signable.java | 14 +++++++++ .../main/java/net/jsign/nuget/NugetFile.java | 29 +++++++++++++++++++ .../test/java/net/jsign/NugetSignerTest.java | 7 +++++ .../test/java/net/jsign/SignatureAssert.java | 15 +++++++++- 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java b/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java index f29401e6..831211c1 100644 --- a/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java +++ b/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java @@ -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 @@ -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 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) { @@ -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 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]); @@ -501,7 +507,7 @@ private void verify(CMSSignedData signedData) throws SignatureException, Operato * * @return the authenticated attributes */ - private AttributeTable createAuthenticatedAttributes() { + private List createAuthenticatedAttributes() { List attributes = new ArrayList<>(); SpcStatementType spcStatementType = new SpcStatementType(AuthenticodeObjectIdentifiers.SPC_INDIVIDUAL_SP_KEY_PURPOSE_OBJID); @@ -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; } /** diff --git a/jsign-core/src/main/java/net/jsign/Signable.java b/jsign-core/src/main/java/net/jsign/Signable.java index 7b8222b6..416b80bc 100644 --- a/jsign-core/src/main/java/net/jsign/Signable.java +++ b/jsign-core/src/main/java/net/jsign/Signable.java @@ -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; @@ -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 createSignedAttributes(X509Certificate certificate) throws CertificateEncodingException { + return new ArrayList<>(); + } + /** * Checks if the specified certificate is suitable for signing the file. * diff --git a/jsign-core/src/main/java/net/jsign/nuget/NugetFile.java b/jsign-core/src/main/java/net/jsign/nuget/NugetFile.java index bfd91cd1..60807983 100644 --- a/jsign-core/src/main/java/net/jsign/nuget/NugetFile.java +++ b/jsign-core/src/main/java/net/jsign/nuget/NugetFile.java @@ -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; @@ -44,6 +54,7 @@ * A NuGet package. * * @see NuGet Package Signatures Technical Specification + * @see NuGet Repository Signatures and Countersignatures Technical Specification * * @author Sebastian Stamm * @since 7.0 @@ -117,6 +128,24 @@ public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) { throw new UnsupportedOperationException(); // not applicable here } + @Override + public List createSignedAttributes(X509Certificate certificate) throws CertificateEncodingException { + List 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 getSignatures() throws IOException { if (centralDirectory.entries.containsKey(SIGNATURE_ENTRY)) { diff --git a/jsign-core/src/test/java/net/jsign/NugetSignerTest.java b/jsign-core/src/test/java/net/jsign/NugetSignerTest.java index 17070ff5..3407ebeb 100644 --- a/jsign-core/src/test/java/net/jsign/NugetSignerTest.java +++ b/jsign-core/src/test/java/net/jsign/NugetSignerTest.java @@ -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; @@ -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); } } diff --git a/jsign-core/src/test/java/net/jsign/SignatureAssert.java b/jsign-core/src/test/java/net/jsign/SignatureAssert.java index d9407fab..f88b9f56 100644 --- a/jsign-core/src/test/java/net/jsign/SignatureAssert.java +++ b/jsign-core/src/test/java/net/jsign/SignatureAssert.java @@ -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; @@ -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)); + } } } @@ -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); + } }