org.apache.sshd.common.config.keys.loader.pem.ECDSAPEMResourceKeyPairParser Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sshd.common.config.keys.loader.pem;
import java.io.IOException;
import java.io.InputStream;
import java.io.StreamCorruptedException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchProviderException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.cipher.ECCurves;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.io.NoCloseInputStream;
import org.apache.sshd.common.util.io.der.ASN1Object;
import org.apache.sshd.common.util.io.der.ASN1Type;
import org.apache.sshd.common.util.io.der.DERParser;
import org.apache.sshd.common.util.security.SecurityUtils;
/**
* @author Apache MINA SSHD Project
* @see RFC 5915
*/
public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairParser {
public static final String BEGIN_MARKER = "BEGIN EC PRIVATE KEY";
public static final List BEGINNERS = Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER));
public static final String END_MARKER = "END EC PRIVATE KEY";
public static final List ENDERS = Collections.unmodifiableList(Collections.singletonList(END_MARKER));
/**
* @see RFC-3279 section 2.3.5
*/
public static final String ECDSA_OID = "1.2.840.10045.2.1";
public static final ECDSAPEMResourceKeyPairParser INSTANCE = new ECDSAPEMResourceKeyPairParser();
public ECDSAPEMResourceKeyPairParser() {
super(KeyUtils.EC_ALGORITHM, ECDSA_OID, BEGINNERS, ENDERS);
}
@Override
public Collection extractKeyPairs(
SessionContext session, NamedResource resourceKey,
String beginMarker, String endMarker,
FilePasswordProvider passwordProvider,
InputStream stream, Map headers)
throws IOException, GeneralSecurityException {
KeyPair kp = parseECKeyPair(stream, false);
return Collections.singletonList(kp);
}
public static KeyPair parseECKeyPair(
InputStream inputStream, boolean okToClose)
throws IOException, GeneralSecurityException {
try (DERParser parser = new DERParser(NoCloseInputStream.resolveInputStream(inputStream, okToClose))) {
return parseECKeyPair(null, parser);
}
}
/**
* @param curve The {@link ECCurves curve} represented by this data (in case it was optional and
* somehow known externally) if {@code null} then it is assumed to be part of the
* parsed data. then it is assumed to be part of the data.
* @param parser The {@link DERParser} for the data
* @return The parsed {@link KeyPair}
* @throws IOException If failed to parse the data
* @throws GeneralSecurityException If failed to generate the keys
*/
public static KeyPair parseECKeyPair(ECCurves curve, DERParser parser)
throws IOException, GeneralSecurityException {
ASN1Object sequence = parser.readObject();
Map.Entry spec = decodeECPrivateKeySpec(curve, sequence);
if (!SecurityUtils.isECCSupported()) {
throw new NoSuchProviderException("ECC not supported");
}
KeyFactory kf = SecurityUtils.getKeyFactory(KeyUtils.EC_ALGORITHM);
ECPublicKey pubKey = (ECPublicKey) kf.generatePublic(spec.getKey());
ECPrivateKey prvKey = (ECPrivateKey) kf.generatePrivate(spec.getValue());
return new KeyPair(pubKey, prvKey);
}
/**
*
* ASN.1 syntax according to RFC 5915 is:
*
*
*
*
*
* ECPrivateKey ::= SEQUENCE {
* version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
* privateKey OCTET STRING,
* parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
* publicKey [1] BIT STRING OPTIONAL
* }
*
*
*
* ECParameters syntax according to RFC5480:
*
*
*
*
*
* ECParameters ::= CHOICE {
* namedCurve OBJECT IDENTIFIER
* -- implicitCurve NULL
* -- specifiedCurve SpecifiedECDomain
* }
*
*
*
* @param curve The {@link ECCurves curve} represented by this data (in case it was optional and somehow
* known externally) if {@code null} then it is assumed to be part of the parsed data.
* @param sequence The {@link ASN1Object} sequence containing the DER encoded data
* @return The decoded {@link SimpleImmutableEntry} of {@link ECPublicKeySpec} and
* {@link ECPrivateKeySpec}
* @throws IOException If failed to to decode the DER stream
*/
public static Map.Entry decodeECPrivateKeySpec(ECCurves curve, ASN1Object sequence)
throws IOException {
ASN1Type objType = (sequence == null) ? null : sequence.getObjType();
if (!ASN1Type.SEQUENCE.equals(objType)) {
throw new IOException("Invalid DER: not a sequence: " + objType);
}
try (DERParser parser = sequence.createParser()) {
Map.Entry result = decodeECPrivateKeySpec(curve, parser);
ECPrivateKeySpec prvSpec = result.getKey();
ASN1Object publicData = result.getValue();
ECPoint w = (publicData == null) ? decodeECPublicKeyValue(parser) : decodeECPointData(publicData);
ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, prvSpec.getParams());
return new SimpleImmutableEntry<>(pubSpec, prvSpec);
}
}
/*
* According to https://tools.ietf.org/html/rfc5915 - section 3
*
* ECPrivateKey ::= SEQUENCE {
* version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
* privateKey OCTET STRING,
* parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
* publicKey [1] BIT STRING OPTIONAL
* }
*/
public static Map.Entry decodeECPrivateKeySpec(ECCurves curve, DERParser parser)
throws IOException {
// see openssl asn1parse -inform PEM -in ...file... -dump
ASN1Object versionObject = parser.readObject();
if (versionObject == null) {
throw new StreamCorruptedException("No version");
}
/*
* According to https://tools.ietf.org/html/rfc5915 - section 3
*
* For this version of the document, it SHALL be set to ecPrivkeyVer1,
* which is of type INTEGER and whose value is one (1)
*/
BigInteger version = versionObject.asInteger();
if (!BigInteger.ONE.equals(version)) {
throw new StreamCorruptedException("Bad version value: " + version);
}
ASN1Object keyObject = parser.readObject();
if (keyObject == null) {
throw new StreamCorruptedException("No private key value");
}
ASN1Type objType = keyObject.getObjType();
if (!ASN1Type.OCTET_STRING.equals(objType)) {
throw new StreamCorruptedException("Non-matching private key object type: " + objType);
}
/*
* According to https://tools.ietf.org/html/rfc5915 - section 3
*
* parameters specifies the elliptic curve domain parameters associated to the private key. The type
* ECParameters is discussed in [RFC5480]. As specified in [RFC5480], only the namedCurve CHOICE is permitted.
* namedCurve is an object identifier that fully identifies the required values for a particular set of elliptic
* curve domain parameters. Though the ASN.1 indicates that the parameters field is OPTIONAL, implementations
* that conform to this document MUST always include the parameters field.
*/
Map.Entry result = parseCurveParameter(parser);
ECCurves namedParam = (result == null) ? null : result.getKey();
if (namedParam == null) {
if (curve == null) {
throw new StreamCorruptedException("Cannot determine curve type");
}
} else if (curve == null) {
curve = namedParam;
} else if (namedParam != curve) {
throw new StreamCorruptedException("Mismatched provide (" + curve + ") vs. parsed curve (" + namedParam + ")");
}
BigInteger s = ECCurves.octetStringToInteger(keyObject.getPureValueBytes());
ECPrivateKeySpec keySpec = new ECPrivateKeySpec(s, curve.getParameters());
return new SimpleImmutableEntry<>(keySpec, (result == null) ? null : result.getValue());
}
public static Map.Entry parseCurveParameter(DERParser parser) throws IOException {
return parseCurveParameter(parser.readObject());
}
public static Map.Entry parseCurveParameter(ASN1Object paramsObject) throws IOException {
if (paramsObject == null) {
return null;
}
ASN1Type objType = paramsObject.getObjType();
if (objType == ASN1Type.NULL) {
return null;
}
List curveOID;
try (DERParser paramsParser = paramsObject.createParser()) {
ASN1Object namedCurve = paramsParser.readObject();
if (namedCurve == null) {
throw new StreamCorruptedException("Missing named curve parameter");
}
/*
* The curve OID is OPTIONAL - if it is not there then the
* public key data replaces it
*/
objType = namedCurve.getObjType();
if (objType == ASN1Type.BIT_STRING) {
return new SimpleImmutableEntry<>(null, namedCurve);
}
curveOID = namedCurve.asOID();
}
ECCurves curve = ECCurves.fromOIDValue(curveOID);
if (curve == null) {
throw new StreamCorruptedException("Unknown curve OID: " + curveOID);
}
return new SimpleImmutableEntry<>(curve, null);
}
/**
*
* ASN.1 syntax according to rfc5915 is:
*
*
*
*
*
* publicKey [1] BIT STRING OPTIONAL
*
*
*
* @param parser The {@link DERParser} assumed to be positioned at the start of the data
* @return The encoded {@link ECPoint}
* @throws IOException If failed to create the point
*/
public static final ECPoint decodeECPublicKeyValue(DERParser parser) throws IOException {
return decodeECPublicKeyValue(parser.readObject());
}
public static final ECPoint decodeECPublicKeyValue(ASN1Object dataObject) throws IOException {
// see openssl asn1parse -inform PEM -in ...file... -dump
if (dataObject == null) {
throw new StreamCorruptedException("No public key data bytes");
}
/*
* According to https://tools.ietf.org/html/rfc5915
*
* Though the ASN.1 indicates publicKey is OPTIONAL, implementations
* that conform to this document SHOULD always include the publicKey field
*/
try (DERParser dataParser = dataObject.createParser()) {
return decodeECPointData(dataParser.readObject());
}
}
public static final ECPoint decodeECPointData(ASN1Object pointData) throws IOException {
if (pointData == null) {
throw new StreamCorruptedException("Missing public key data parameter");
}
ASN1Type objType = pointData.getObjType();
if (!ASN1Type.BIT_STRING.equals(objType)) {
throw new StreamCorruptedException("Non-matching public key object type: " + objType);
}
// see https://tools.ietf.org/html/rfc5480#section-2.2
byte[] octets = pointData.getValue();
return ECCurves.octetStringToEcPoint(octets);
}
}