【发布时间】:2020-12-29 02:45:33
【问题描述】:
我无法弄清楚如何正确读取 pem 文件的私钥。我在stackoverflow上浏览了不同的主题,但我找不到解决方案。我想要实现的是从类路径中读取 pkcs#8 编码文件中的加密私钥,并将其作为密钥条目加载到内存密钥库中。下面是我尝试解析的示例私钥,密码是secret。它纯粹是为了在这里共享而创建的,因此它不是在生产机器上使用的私钥。
我使用以下命令从 p12 文件创建它:openssl pkcs12 -in key-pair.p12 -out key-pair.pem
有效示例(一次性)密钥对
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQI9/FSBonUacYCAggA
MBQGCCqGSIb3DQMHBAidGkS8wYhOpwSCBMi8JaSYOKudMNNFRpzL7QMIZgFtzDay
MmOroy3lW34dOa7dusqDl4d2gklKcHCpbEaTYxm5aQJ1LuiOdGtFy7HwxOvKU5xz
4qsJoeBIpE0eCTKjQW7/I38DzLXx2wUURqhMWOtFsWZEyR5Dqok3N9mIKKKBXAFG
AwNjlTRW2LyPSttiIUGN01lthjifMWoLTWB1aSGOmGeJRBdSZeqZ15xKneR4H5ja
yE88YcpOHCDKMIxi6ZVoKs7jDQhu8bBKqS8NsYyh1AlP9QkvWNal36jWSzhqYNzk
NRWUOZngfkdbMALVfRtbrC215jHGWVwosPIIs8rkoarRv8s6QWS1Rg3YfQ3qgcRf
s7hkDFKJf3TUXr+askfamV5hc300ZG64+ldX1YxWXY8Vd/wIvHAc/YE/lTyCgYrY
19Am6MNBfp8/kXvzKj+PizB8oNDO4S8sSShEEzOQ5a/+MTC6bqB0DLWYGUqRbjLc
PyYTC2C4i9Agx/GeGVE3c1UdtXiwwnt2XUn7Y1YGqABk0xGIY4J1NFTbSOxKl9hO
arwopAFrZU5nsjjFzv1DJvhfQWnYX18kPSKNHDlia019M118qZ8ERwD9tH8ix9Fa
R2tQdxn1aRGmvXSw+zFkbWD8aWs9n/B+QN1yllJqVoWypOld1yj+fVYYnYOtV1gK
eiygrtrh3JJCvLbEQl4nOgJM3PlEtfBHSaunehIXQMD1z/NDUqgBYjuDPyqRxJeH
Va5k72Nds5PeySKJJnICB3nZKjqgfLhNUrXa1SAQ4vqr0Ik/Lu9P7T+B1XiYwuUT
a20+bxi/x89ZZqwp3jnDuHup7XcO1MtqsoOKP/JgkjVMesb8Q1W8i2dXzg+l4gkk
l1ipreEGtT1YfFTq0DFelz6CjZFLDlGGeGWob94sW94DWTW0nsLPhQWEnwW1CcyJ
oJbJdDEgdiIbRJoABDkTuVXLwTlgzHSHh6zeJvNvcojI7UI3nWYCVYvD3kwghXiP
67sKGL3ug7PFDqLia46AudGY7CFh4+wpxyH+fidLC3FMdkDBA6xR6mGgEjRLXR9M
TnJ/eSYP7eqYZeKn9EarcI7v1zM2IG0/PDQCetiI0ABiHpdKyRQuuiEavp3xC5Vi
h7UmJNYt8Zsz3rwqAQ4FR2+Su5R34OOdRmxTaYLe96PXTpLcLef5TkYixSY7Tzgd
PMyRxRPrywklUEFe4KK/KOcdolxybfsIsxQnupLAMEsO7/Cs7mouNHISK51haDRc
vNbKQ5E4xOq1U4ThW5dHR29cGZillfmMzj05ZQh3ZX2TQJP45ahFET3v9kInWCwQ
8atqclVPOSnASsJZ0PxjYgKZuY8QWYM6zpfWyWnfu/CHhWbRS/qX8T1ow2SMyPBL
CQbZ+MhcdP0IrjoXhDFQsns16i/BPK5TTVqtEC2ywDf5P4/BOEZkySG9YNOd6THp
VA/dVPafzmLy3ltqH+jG8ZH2+RtWx7kwBjiDWs5cF33BFrPS7AZlzMzZoCHLXD/r
T/SmisybUKHMqri0x0RHeIByW0hogSByWiyIn8POabDzJV6Df9nQPziDGcSsvWfG
7q+hizh6+nnXOY+GZx3ptwg9mA9R4QyCiFNQradOaXSPxyEL2IC77/srFfVEIaU4
SRo=
-----END ENCRYPTED PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDTjCCAjagAwIBAgIEVnHI3TANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJO
TDEVMBMGA1UEChMMVGh1bmRlcmJlcnJ5MRIwEAYDVQQLEwlBbXN0ZXJkYW0xDjAM
BgNVBAMTBUhha2FuMB4XDTIwMDgzMTA4MDczOVoXDTMwMDgyOTA4MDczOVowSDEL
MAkGA1UEBhMCTkwxFTATBgNVBAoTDFRodW5kZXJiZXJyeTESMBAGA1UECxMJQW1z
dGVyZGFtMQ4wDAYDVQQDEwVIYWthbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAJZ+pqirEqEk4k1ow8vryld79oCO4P/1y+v60CkLpS2MpeHE3BogTr7g
WWP5HdHBMU5l8yT5tXuyVZZqgyzo0q2Sdm7GGrNunHf6Z8vPQFu69sQC1utDM9u8
ZFKiKsTNJ+5QS6KsOtlACyhaLoaPAWVjtvueMjVwmM9hfv/Gq6VyyuBm2x1C4HTj
zPCLHE2G1D13EJaWsvyZLGSbl0GGXZGPhaDd/vnw5TW36mvNTWW+37ZIEk4kXANa
FUNsJemm8HjB/PzHs3/SXGxuD8NKobg3+cNXYwAz2s2DI0W6Xw2g5bbrMQAdBRvn
9/kNftMymDORw3RGwDM2ld4zQfIkNrkCAwEAAaNAMD4wHQYDVR0OBBYEFHhT7ATg
oVVFIsglxD/1iUBRB6gDMB0GA1UdEQEB/wQTMBGCCWxvY2FsaG9zdIcEfwAAATAN
BgkqhkiG9w0BAQsFAAOCAQEAhVWH/CgZ0ZNmTfiFAOnWdaZVaa7vAFPT2YbXuvlY
YIRlru0B/zn2Mfwmn5W2o1CqoBuhyfErkkF4aRM1vduIirUjlcH4+cFXrV2gtlnf
eWTg/sJJmYzkJTGeOIqRlB1HKCqoeNCrykkcsikECQ1nCqr1qLh9DXsUgWVW57YW
qvP1P8xOO2/J9shMB6lOhftpawrqZ2hNG8fqMKjVVuUpFBNR+WODQ/rRRtqa6uU2
V8aOOZx1QJUkTdN76YOCuGET7edevjpdbRXde+HQN6mbT9OLxSZHO0aQrDyDmNhp
aVHuQn/KtYNWCZ78XKK8wtVnflmfqE/c9xO1n/EcVvLCdg==
-----END CERTIFICATE-----
我知道 BouncyCastle 能够解析它,但我想避免使用额外的库。所以我想知道是否可以仅使用普通 jdk 或其他一些轻量级库。
我已经能够使用以下页眉/页脚解析私钥:
-----BEGIN PRIVATE KEY-----
*
-----END PRIVATE KEY-----
和
-----BEGIN RSA PRIVATE KEY-----
*
-----END RSA PRIVATE KEY-----
我已经使用以下 sn-p 来完成此操作:
import com.hierynomus.asn1.ASN1InputStream;
import com.hierynomus.asn1.encodingrules.der.DERDecoder;
import com.hierynomus.asn1.types.ASN1Object;
import com.hierynomus.asn1.types.constructed.ASN1Sequence;
import com.hierynomus.asn1.types.primitive.ASN1Integer;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class App {
private static final String KEYSTORE_TYPE = "PKCS12";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String CERTIFICATE_TYPE = "X.509";
private static final Pattern CERTIFICATE_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----", Pattern.DOTALL);
private static final Pattern PRIVATE_KEY_PATTERN = Pattern.compile("-----BEGIN PRIVATE KEY-----(.*?)-----END PRIVATE KEY-----", Pattern.DOTALL);
private static final Pattern ENCRYPTED_PRIVATE_KEY_PATTERN = Pattern.compile("-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END ENCRYPTED PRIVATE KEY-----", Pattern.DOTALL);
private static final Pattern RSA_PRIVATE_KEY_PATTERN = Pattern.compile("-----BEGIN RSA PRIVATE KEY-----(.*?)-----END RSA PRIVATE KEY-----", Pattern.DOTALL);
private static final String NEW_LINE = "\n";
private static final String EMPTY = "";
public static void main(String[] args) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, InvalidKeySpecException {
String privateKeyContent = "";
String certificateContent = "";
PrivateKey privateKey = parsePrivateKey(privateKeyContent);
Certificate[] certificates = parseCertificate(certificateContent).values()
.toArray(new Certificate[]{});
KeyStore keyStore = createEmptyKeyStore();
keyStore.setKeyEntry("client", privateKey, null, certificates);
}
private static KeyStore createEmptyKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
keyStore.load(null, null);
return keyStore;
}
private static PrivateKey parsePrivateKey(String identityContent) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
KeySpec keySpec = null;
Matcher privateKeyMatcher = PRIVATE_KEY_PATTERN.matcher(identityContent);
if (privateKeyMatcher.find()) {
String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
keySpec = new PKCS8EncodedKeySpec(decodedPrivateKeyContent);
}
privateKeyMatcher = RSA_PRIVATE_KEY_PATTERN.matcher(identityContent);
if (privateKeyMatcher.find()) {
String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
keySpec = getKeySpecFromAsn1StructuredData(decodedPrivateKeyContent);
}
privateKeyMatcher = ENCRYPTED_PRIVATE_KEY_PATTERN.matcher(identityContent);
if (privateKeyMatcher.find()) {
String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
keySpec = null; //TODO
}
Objects.requireNonNull(keySpec);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePrivate(keySpec);
}
private static KeySpec getKeySpecFromAsn1StructuredData(byte[] decodedPrivateKeyContent) throws IOException {
try(ByteArrayInputStream privateKeyAsInputStream = new ByteArrayInputStream(decodedPrivateKeyContent)) {
ASN1InputStream stream = new ASN1InputStream(new DERDecoder(), privateKeyAsInputStream);
ASN1Sequence asn1Sequence = stream.readObject();
if (asn1Sequence.getValue().size() < 9) {
throw new RuntimeException("Parsed key content doesn't have the minimum required sequence size of 9");
}
BigInteger modulus = extractIntValueFrom(asn1Sequence.get(1));
BigInteger publicExponent = extractIntValueFrom(asn1Sequence.get(2));
BigInteger privateExponent = extractIntValueFrom(asn1Sequence.get(3));
BigInteger primeP = extractIntValueFrom(asn1Sequence.get(4));
BigInteger primeQ = extractIntValueFrom(asn1Sequence.get(5));
BigInteger primeExponentP = extractIntValueFrom(asn1Sequence.get(6));
BigInteger primeExponentQ = extractIntValueFrom(asn1Sequence.get(7));
BigInteger crtCoefficient = extractIntValueFrom(asn1Sequence.get(8));
return new RSAPrivateCrtKeySpec(
modulus, publicExponent, privateExponent,
primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient
);
}
}
private static BigInteger extractIntValueFrom(ASN1Object<?> asn1Object) {
if (asn1Object instanceof ASN1Integer) {
return ((ASN1Integer) asn1Object).getValue();
} else {
throw new RuntimeException(String.format(
"Unable to parse the provided value of the object type [%s]. The type should be an instance of [%s]",
asn1Object.getClass().getName(), ASN1Integer.class.getName())
);
}
}
private static Map<String, Certificate> parseCertificate(String certificateContent) throws IOException, CertificateException {
Map<String, Certificate> certificates = new HashMap<>();
Matcher certificateMatcher = CERTIFICATE_PATTERN.matcher(certificateContent);
while (certificateMatcher.find()) {
String sanitizedCertificate = certificateMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
byte[] decodedCertificate = Base64.getDecoder().decode(sanitizedCertificate);
try(ByteArrayInputStream certificateAsInputStream = new ByteArrayInputStream(decodedCertificate)) {
CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_TYPE);
Certificate certificate = certificateFactory.generateCertificate(certificateAsInputStream);
certificates.put(UUID.randomUUID().toString(), certificate);
}
}
return certificates;
}
}
我还需要处理 asn.1 编码的私钥。我可以使用 BouncyCastle,但我又想避免它,因为它很重。我从这个主题中得到了启发:https://stackoverflow.com/a/42733858/6777695 但从 jdk 11 开始,DerInputStream 和 DerValue 就不再可用了。并且应该避免使用sun包......所以我发现asn-one java library可以解析asn.1编码的私钥,并且效果很好。
我已经分享了我处理其他案件的完整经验,所以当有人想帮助我时,尝试一下会更容易。
从以下文章:ASN.1 key structures in DER and PEM 我了解到具有以下页眉/页脚的私钥:-----BEGIN ENCRYPTED PRIVATE KEY----- * -----END ENCRYPTED PRIVATE KEY----- 也是一个 asn.1 编码的私钥,具有以下结构:
EncryptedPrivateKeyInfo ::= SEQUENCE {
encryptionAlgorithm EncryptionAlgorithmIdentifier,
encryptedData EncryptedData
}
EncryptionAlgorithmIdentifier ::= AlgorithmIdentifier
EncryptedData ::= OCTET STRING
我也尝试了How to read a password encrypted key with java? 此处提供的答案,但这些也没有奏效。但是我不太确定如何将其正确解析为 java 对象并将其作为 KeySpec 加载。所以欢迎任何帮助!
======>
根据dave_thompson_085的输入,于2020年9月12日更新进度
Matcher privateKeyMatcher = Pattern.compile("-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END ENCRYPTED PRIVATE KEY-----", Pattern.DOTALL)
.matcher(identityContent);
if (privateKeyMatcher.find()) {
String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
try (ByteArrayInputStream privateKeyAsInputStream = new ByteArrayInputStream(decodedPrivateKeyContent)) {
ASN1InputStream stream = new ASN1InputStream(new DERDecoder(), privateKeyAsInputStream);
ASN1Sequence asn1Sequence = stream.readObject();
ASN1OctetString privateKeyAsOctetString = (ASN1OctetString) asn1Sequence.get(1);
ASN1OctetString saltAsOctetString = (ASN1OctetString) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(0)).get(1)).get(0);
ASN1OctetString initializationVectorAsOctec = ((ASN1OctetString) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(1)).get(1));
ASN1Integer iterationCount = (ASN1Integer) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(0)).get(1)).get(1);
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVectorAsOctec.getValue());
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1");
EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(secretKeyFactory.getAlgorithm(), privateKeyAsOctetString.getValue());
int keyLength = encryptedPrivateKeyInfo.getEncoded().length;
PBEKeySpec pbeKeySpec = new PBEKeySpec(keyPassword, saltAsOctetString.getValue(), iterationCount.getValue().intValue(), keyLength);
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding/");
SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
PKCS8EncodedKeySpec keySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
}
}
导致以下异常:java.security.InvalidKeyException: Wrong algorithm: DESede or TripleDES required 上声明cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
======>
根据Michael Fehr的输入,于2020年9月13日更新进度
Matcher privateKeyMatcher = Pattern.compile("-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END ENCRYPTED PRIVATE KEY-----", Pattern.DOTALL)
.matcher(identityContent);
if (privateKeyMatcher.find()) {
String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
try (ByteArrayInputStream privateKeyAsInputStream = new ByteArrayInputStream(decodedPrivateKeyContent)) {
ASN1InputStream stream = new ASN1InputStream(new DERDecoder(), privateKeyAsInputStream);
ASN1Sequence asn1Sequence = stream.readObject();
ASN1OctetString privateKeyAsOctetString = (ASN1OctetString) asn1Sequence.get(1);
ASN1OctetString saltAsOctetString = (ASN1OctetString) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(0)).get(1)).get(0);
ASN1OctetString initializationVectorAsOctec = ((ASN1OctetString) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(1)).get(1));
ASN1Integer iterationCount = (ASN1Integer) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(0)).get(1)).get(1);
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVectorAsOctec.getValue());
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1");
EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(secretKeyFactory.getAlgorithm(), privateKeyAsOctetString.getValue());
int keyLength = 24 * 8;
PBEKeySpec pbeKeySpec = new PBEKeySpec(keyPassword, saltAsOctetString.getValue(), iterationCount.getValue().intValue(), keyLength);
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding/");
SecretKey secretKeyPbkdf = secretKeyFactory.generateSecret(pbeKeySpec);
SecretKey secretKey = new SecretKeySpec(secretKeyPbkdf.getEncoded(), "DESede");
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
PKCS8EncodedKeySpec keySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
}
}
【问题讨论】:
-
试试这个out
-
您在使用本机 Java 类时的主要问题将是以下错误:“PBE 参数解析错误:期望 AES 密码的对象标识符”,因为您的加密密钥是使用 PBKDF + TripleDES 加密的(请参阅标识符:现代Java版本(DES部分)不(更长?)支持的对象标识符des-EDE3-CBC(1 2 840 113549 3 7))。我相信你必须使用 Bouncy Castle 阅读这种钥匙。
-
@MichaelFehr:我不知道你在看哪里,但是 TripleDES/TDEA 仍然广泛用于 PKCS8(还有 PKCS12,但通常在 PKCS12PBE 而不是 PBES2 中),对于 short 数据,如密钥文件,由 Java 加密支持,算法名称为 DESede。虽然 PKCS8+PKCS5 格式 允许 AES,但我不知道目前有人在使用它。 JCE 甚至支持单一/原始 DES,尽管它不应再使用,并且在 JSSE for SSL/TLS 中默认禁用。
-
@dave_thompson_085:我尝试使用以下代码解析 PEM(剥离页眉和页脚行后):“EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(Base64.getMimeDecoder().decode(encrypted)); "并收到异常“java.io.IOException:PBE 参数解析错误:期望 AES 密码的对象标识符”。因此,对于 EncryptedPrivateKeyInfo 似乎没有可用的 TripleDES/TDEA 密码,尽管在“独立”使用它时它就在那里。正如您在回答中所写,我们必须自己进行密钥推导和解密。感谢您的许可。
-
@MichaelFehr:啊! That's a known problem but it changed. 以前不处理any PBES2 参数;我没有注意到在 11.0.1 中他们将其修复为仅适用于 AES-128 和 AES-256(可能并非巧合,JCE 直接实施的唯一 PBES2 方案)所以我没有认识到新症状。但是我看到他们现在计划为明年到期的 j16 修复它。耶!
标签: java ssl encryption keystore pem