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