/*
* 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.Collection;
import java.util.Collections;
import java.util.List;
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.util.Pair;
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 <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairParser {
public static final String BEGIN_MARKER = "BEGIN EC PRIVATE KEY";
public static final List<String> BEGINNERS =
Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER));
public static final String END_MARKER = "END EC PRIVATE KEY";
public static final List<String> ENDERS =
Collections.unmodifiableList(Collections.singletonList(END_MARKER));
/**
* @see <A HREF="https://tools.ietf.org/html/rfc3279#section-2.3.5">RFC-3279 section 2.3.5</A>
*/
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<KeyPair> extractKeyPairs(
String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, InputStream stream)
throws IOException, GeneralSecurityException {
Pair<ECPublicKeySpec, ECPrivateKeySpec> spec = decodeECPrivateKeySpec(stream, false);
if (!SecurityUtils.isECCSupported()) {
throw new NoSuchProviderException("ECC not supported");
}
KeyFactory kf = SecurityUtils.getKeyFactory(KeyUtils.EC_ALGORITHM);
ECPublicKey pubKey = (ECPublicKey) kf.generatePublic(spec.getFirst());
ECPrivateKey prvKey = (ECPrivateKey) kf.generatePrivate(spec.getSecond());
KeyPair kp = new KeyPair(pubKey, prvKey);
return Collections.singletonList(kp);
}
/**
* <P>ASN.1 syntax according to rfc5915 is:</P></BR>
* <PRE><CODE>
* ECPrivateKey ::= SEQUENCE {
* version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
* privateKey OCTET STRING,
* parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
* publicKey [1] BIT STRING OPTIONAL
* }
* </CODE></PRE>
* <P><I>ECParameters</I> syntax according to RFC5480:</P></BR>
* <PRE><CODE>
* ECParameters ::= CHOICE {
* namedCurve OBJECT IDENTIFIER
* -- implicitCurve NULL
* -- specifiedCurve SpecifiedECDomain
* }
* </CODE></PRE>
* @param inputStream The {@link InputStream} containing the DER encoded data
* @param okToClose {@code true} if OK to close the DER stream once parsing complete
* @return The decoded {@link Pair} of {@link ECPublicKeySpec} and {@link ECPrivateKeySpec}
* @throws IOException If failed to to decode the DER stream
*/
public static Pair<ECPublicKeySpec, ECPrivateKeySpec> decodeECPrivateKeySpec(InputStream inputStream, boolean okToClose) throws IOException {
ASN1Object sequence;
try (DERParser parser = new DERParser(NoCloseInputStream.resolveInputStream(inputStream, okToClose))) {
sequence = parser.readObject();
}
if (!ASN1Type.SEQUENCE.equals(sequence.getObjType())) {
throw new IOException("Invalid DER: not a sequence: " + sequence.getObjType());
}
// Parse inside the sequence
try (DERParser parser = sequence.createParser()) {
ECPrivateKeySpec prvSpec = decodeECPrivateKeySpec(parser);
ECCurves curve = ECCurves.fromCurveParameters(prvSpec.getParams());
if (curve == null) {
throw new StreamCorruptedException("Unknown curve");
}
ECPoint w = decodeECPublicKeyValue(curve, parser);
ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, prvSpec.getParams());
return new Pair<>(pubSpec, prvSpec);
}
}
public static final ECPrivateKeySpec decodeECPrivateKeySpec(DERParser parser) throws IOException {
// see openssl asn1parse -inform PEM -in ...file... -dump
ASN1Object versionObject = parser.readObject(); // Skip version
if (versionObject == null) {
throw new StreamCorruptedException("No version");
}
// as per RFC-5915 section 3
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);
}
ASN1Object paramsObject = parser.readObject();
if (paramsObject == null) {
throw new StreamCorruptedException("No parameters value");
}
// TODO make sure params object tag is 0xA0
final List<Integer> curveOID;
try (DERParser paramsParser = paramsObject.createParser()) {
ASN1Object namedCurve = paramsParser.readObject();
if (namedCurve == null) {
throw new StreamCorruptedException("Missing named curve parameter");
}
curveOID = namedCurve.asOID();
}
ECCurves curve = ECCurves.fromOIDValue(curveOID);
if (curve == null) {
throw new StreamCorruptedException("Unknown curve OID: " + curveOID);
}
BigInteger s = ECCurves.octetStringToInteger(keyObject.getPureValueBytes());
return new ECPrivateKeySpec(s, curve.getParameters());
}
/**
* <P>ASN.1 syntax according to rfc5915 is:</P></BR>
* <PRE>
* publicKey [1] BIT STRING OPTIONAL
* </PRE>
* @param curve The {@link ECCurves} curve
* @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(ECCurves curve, DERParser parser) throws IOException {
// see openssl asn1parse -inform PEM -in ...file... -dump
ASN1Object dataObject = parser.readObject();
if (dataObject == null) {
throw new StreamCorruptedException("No public key data bytes");
}
try (DERParser dataParser = dataObject.createParser()) {
ASN1Object pointData = dataParser.readObject();
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);
}
}
}