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}