001/**
002 * Copyright 2023 Emmanuel Bourg
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.jca;
018
019import java.io.ByteArrayInputStream;
020import java.io.IOException;
021import java.security.GeneralSecurityException;
022import java.security.KeyStoreException;
023import java.security.UnrecoverableKeyException;
024import java.security.cert.Certificate;
025import java.security.cert.CertificateException;
026import java.security.cert.CertificateFactory;
027import java.security.cert.X509Certificate;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import java.util.function.Function;
033import java.util.stream.Collectors;
034import javax.smartcardio.CardException;
035
036import org.bouncycastle.asn1.ASN1Encoding;
037import org.bouncycastle.asn1.DERNull;
038import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
039import org.bouncycastle.asn1.x509.DigestInfo;
040
041import net.jsign.DigestAlgorithm;
042
043/**
044 * Signing service using an OpenPGP smart card. OpenPGP cards contain up to 3 keys (for signing, authentication
045 * and encryption), but all can be used for code signing. The card may contain an X.509 certificate for each key,
046 * the intermediate certificates have to be provided externally.
047 *
048 * @since 5.0
049 */
050public class OpenPGPCardSigningService implements SigningService {
051
052    private final OpenPGPCard pgpcard;
053
054    /** Source for the certificates */
055    private final Function<String, Certificate[]> certificateStore;
056
057    public OpenPGPCardSigningService(String pin, Function<String, Certificate[]> certificateStore) throws CardException {
058        OpenPGPCard pgpcard = OpenPGPCard.getCard();
059        if (pgpcard == null) {
060            throw new CardException("OpenPGP card not found");
061        }
062
063        this.certificateStore = certificateStore;
064        this.pgpcard = pgpcard;
065        this.pgpcard.verify(pin);
066    }
067
068    @Override
069    public String getName() {
070        return "OPENPGP";
071    }
072
073    @Override
074    public List<String> aliases() throws KeyStoreException {
075        try {
076            Set<OpenPGPCard.Key> keys = pgpcard.getAvailableKeys();
077            return keys.stream().map(Enum::name).collect(Collectors.toList());
078        } catch (CardException e) {
079            throw new KeyStoreException(e);
080        }
081    }
082
083    @Override
084    public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
085        Map<String, Certificate> certificates = new LinkedHashMap<>();
086
087        // add the certificate from the card
088        try {
089            OpenPGPCard.Key key = OpenPGPCard.Key.valueOf(alias);
090            ByteArrayInputStream data = new ByteArrayInputStream(pgpcard.getCertificate(key));
091            data.mark(0);
092
093            if (data.available() > 0) {
094                CertificateFactory factory = CertificateFactory.getInstance("X.509");
095                try {
096                    // The format of the certificate on the card is unspecified, let's be optimistic and assume
097                    // it's a full chain in PKCS#7 format (unlikely considering the size constraints of the card
098                    // but who knows, some day maybe)
099                    Certificate[] chain = factory.generateCertPath(data).getCertificates().toArray(new Certificate[0]);
100                    for (Certificate certificate : chain) {
101                        String subject = ((X509Certificate) certificate).getSubjectX500Principal().getName();
102                        certificates.put(subject, certificate);
103                    }
104                } catch (CertificateException e) {
105                    data.reset();
106                    Certificate certificate = factory.generateCertificate(data);
107                    String subject = ((X509Certificate) certificate).getSubjectX500Principal().getName();
108                    certificates.put(subject, certificate);
109                }
110            }
111        } catch (CardException | CertificateException e) {
112            throw new KeyStoreException(e);
113        }
114
115        // add the certificates from the certificate store
116        if (certificateStore != null) {
117            for (Certificate certificate : certificateStore.apply(alias)) {
118                String subject = ((X509Certificate) certificate).getSubjectX500Principal().getName();
119                certificates.put(subject, certificate);
120            }
121        }
122
123        return certificates.values().toArray(new Certificate[0]);
124    }
125
126    @Override
127    public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException {
128        OpenPGPCard.KeyInfo keyInfo;
129        try {
130            keyInfo = pgpcard.getKeyInfo(OpenPGPCard.Key.valueOf(alias));
131        } catch (CardException e) {
132            throw (UnrecoverableKeyException) new UnrecoverableKeyException("Unable to retrieve the info for key " + alias).initCause(e);
133        }
134
135        String algorithm;
136        if (keyInfo.isRSA()) {
137            algorithm = "RSA";
138        } else if (keyInfo.isEC()) {
139            algorithm = "ECDSA";
140        } else {
141            throw new UnrecoverableKeyException("Unsupported key algorithm " + keyInfo.algorithm + " for key " + alias);
142        }
143
144        return new SigningServicePrivateKey(alias, algorithm);
145    }
146
147    @Override
148    public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException {
149        DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with")));
150        byte[] digest = digestAlgorithm.getMessageDigest().digest(data);
151
152        try {
153            byte[] content;
154            if ("RSA".equals(privateKey.getAlgorithm())) {
155                // RSA
156                DigestInfo digestInfo = new DigestInfo(new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE), digest);
157                content = digestInfo.getEncoded(ASN1Encoding.DER);
158            } else {
159                // ECDSA
160                content = digest;
161            }
162            return  pgpcard.sign(OpenPGPCard.Key.valueOf(privateKey.getId()), content);
163        } catch (CardException | IOException e) {
164            throw new GeneralSecurityException(e);
165        }
166    }
167}