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}