001/* 002 * Copyright 2024 Sebastian Stamm 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package net.jsign.nuget; 018 019import java.io.ByteArrayOutputStream; 020import java.io.File; 021import java.io.IOException; 022import java.io.InputStream; 023import java.nio.channels.SeekableByteChannel; 024import java.security.MessageDigest; 025import java.security.cert.CertificateEncodingException; 026import java.security.cert.X509Certificate; 027import java.util.ArrayList; 028import java.util.Base64; 029import java.util.List; 030 031import org.apache.poi.util.IOUtils; 032import org.bouncycastle.asn1.ASN1Object; 033import org.bouncycastle.asn1.DERSet; 034import org.bouncycastle.asn1.cms.Attribute; 035import org.bouncycastle.asn1.esf.CommitmentTypeIndication; 036import org.bouncycastle.asn1.ess.ESSCertIDv2; 037import org.bouncycastle.asn1.ess.SigningCertificateV2; 038import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; 039import org.bouncycastle.asn1.x500.X500Name; 040import org.bouncycastle.asn1.x509.IssuerSerial; 041import org.bouncycastle.cms.CMSProcessableByteArray; 042import org.bouncycastle.cms.CMSSignedData; 043import org.bouncycastle.cms.CMSTypedData; 044 045import net.jsign.ChannelUtils; 046import net.jsign.DigestAlgorithm; 047import net.jsign.Signable; 048import net.jsign.SignatureUtils; 049import net.jsign.zip.CentralDirectory; 050import net.jsign.zip.ZipFile; 051 052/** 053 * A NuGet package. 054 * 055 * @see <a href="https://github.com/NuGet/Home/wiki/Package-Signatures-Technical-Details">NuGet Package Signatures Technical Specification</a> 056 * @see <a href="https://github.com/NuGet/Home/wiki/Repository-Signatures-and-Countersignatures-Technical-Specification">NuGet Repository Signatures and Countersignatures Technical Specification</a> 057 * 058 * @author Sebastian Stamm 059 * @since 7.0 060 */ 061public class NugetFile extends ZipFile implements Signable { 062 063 /** The name of the package signature entry in the archive */ 064 private static final String SIGNATURE_ENTRY = ".signature.p7s"; 065 066 /** 067 * Create an NuGet from the specified file. 068 * 069 * @param file the file to open 070 * @throws IOException if an I/O error occurs 071 */ 072 public NugetFile(File file) throws IOException { 073 super(file); 074 verifyPackage(); 075 } 076 077 /** 078 * Create an NuGet from the specified channel. 079 * 080 * @param channel the channel to read the file from 081 * @throws IOException if an I/O error occurs 082 */ 083 public NugetFile(SeekableByteChannel channel) throws IOException { 084 super(channel); 085 verifyPackage(); 086 } 087 088 private void verifyPackage() throws IOException { 089 if (centralDirectory.entries.get("[Content_Types].xml") == null) { 090 throw new IOException("Invalid NuGet package, [Content_Types].xml is missing"); 091 } 092 } 093 094 @Override 095 public byte[] computeDigest(DigestAlgorithm digestAlgorithm) throws IOException { 096 MessageDigest digest = digestAlgorithm.getMessageDigest(); 097 098 // digest the file records 099 long endOfContentOffset = centralDirectory.centralDirectoryOffset; 100 if (centralDirectory.entries.containsKey(SIGNATURE_ENTRY)) { 101 endOfContentOffset = centralDirectory.entries.get(SIGNATURE_ENTRY).getLocalHeaderOffset(); 102 } 103 ChannelUtils.updateDigest(channel, digest, 0, endOfContentOffset); 104 105 // digest the central directory 106 digest.update(getUnsignedCentralDirectory()); 107 return String.format("Version:1\n\n%s-Hash:%s\n\n", digestAlgorithm.oid, Base64.getEncoder().encodeToString(digest.digest())).getBytes(); 108 } 109 110 /** 111 * Returns a copy of the central directory as if the package was unsigned. 112 */ 113 private byte[] getUnsignedCentralDirectory() throws IOException { 114 CentralDirectory centralDirectory = new CentralDirectory(); 115 centralDirectory.read(channel); 116 centralDirectory.removeEntry(SIGNATURE_ENTRY); 117 return centralDirectory.toBytes(); 118 } 119 120 @Override 121 public CMSTypedData createSignedContent(DigestAlgorithm digestAlgorithm) throws IOException { 122 return new CMSProcessableByteArray(PKCSObjectIdentifiers.data, computeDigest(digestAlgorithm)); 123 } 124 125 @Override 126 public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) { 127 throw new UnsupportedOperationException(); // not applicable here 128 } 129 130 @Override 131 public List<Attribute> createSignedAttributes(X509Certificate certificate) throws CertificateEncodingException { 132 List<Attribute> attributes = new ArrayList<>(); 133 134 CommitmentTypeIndication commitmentTypeIndication = new CommitmentTypeIndication(PKCSObjectIdentifiers.id_cti_ets_proofOfOrigin); 135 attributes.add(new Attribute(PKCSObjectIdentifiers.id_aa_ets_commitmentType, new DERSet(commitmentTypeIndication))); 136 // todo use the id-cti-ets-proofOfReceipt type for repository signatures 137 138 // todo add the nuget-v3-service-index-url and nuget-package-owners attributes for repository signatures 139 140 byte[] certHash = DigestAlgorithm.SHA256.getMessageDigest().digest(certificate.getEncoded()); 141 IssuerSerial issuerSerial = new IssuerSerial(X500Name.getInstance(certificate.getIssuerX500Principal().getEncoded()), certificate.getSerialNumber()); 142 SigningCertificateV2 signingCertificateV2 = new SigningCertificateV2(new ESSCertIDv2(certHash, issuerSerial)); 143 attributes.add(new Attribute(PKCSObjectIdentifiers.id_aa_signingCertificateV2, new DERSet(signingCertificateV2))); 144 145 return attributes; 146 } 147 148 @Override 149 public List<CMSSignedData> getSignatures() throws IOException { 150 if (centralDirectory.entries.containsKey(SIGNATURE_ENTRY)) { 151 InputStream in = getInputStream(SIGNATURE_ENTRY, 1024 * 1024 /* 1MB */); 152 return SignatureUtils.getSignatures(IOUtils.toByteArray(in)); 153 } else { 154 return new ArrayList<>(); 155 } 156 } 157 158 @Override 159 public void setSignature(CMSSignedData signature) throws IOException { 160 if (centralDirectory.entries.containsKey(SIGNATURE_ENTRY)) { 161 removeEntry(SIGNATURE_ENTRY); 162 } 163 164 if (signature != null) { 165 ByteArrayOutputStream out = new ByteArrayOutputStream(); 166 signature.toASN1Structure().encodeTo(out, "DER"); 167 addEntry(SIGNATURE_ENTRY, out.toByteArray(), false); 168 } 169 } 170 171 @Override 172 public void save() throws IOException { 173 } 174}