blob: 5070e737c996b0404448f23f7520dabb080ba2da [file] [log] [blame]
/*
* 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.certificate;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.sshd.common.BaseBuilder;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.OpenSshCertificate;
import org.apache.sshd.common.config.keys.OpenSshCertificate.Type;
import org.apache.sshd.common.config.keys.OpenSshCertificateImpl;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.signature.BuiltinSignatures;
import org.apache.sshd.common.signature.Signature;
import org.apache.sshd.common.signature.SignatureFactory;
import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
/**
* Holds all the data necessary to create a signed OpenSSH Certificate
*/
public class OpenSshCertificateBuilder {
// given a supported key type, map to the concrete OpenSSH Certificate type
protected static final Map<String, String> SIGNATURE_ALGORITHM_MAP
= MapEntryUtils.MapBuilder.<String, String> builder()
.put(KeyPairProvider.SSH_RSA, KeyPairProvider.SSH_RSA_CERT)
.put(KeyPairProvider.SSH_ED25519, KeyPairProvider.SSH_ED25519_CERT)
.put(KeyPairProvider.ECDSA_SHA2_NISTP256, KeyPairProvider.SSH_ECDSA_SHA2_NISTP256_CERT)
.put(KeyPairProvider.ECDSA_SHA2_NISTP384, KeyPairProvider.SSH_ECDSA_SHA2_NISTP384_CERT)
.put(KeyPairProvider.ECDSA_SHA2_NISTP521, KeyPairProvider.SSH_ECDSA_SHA2_NISTP521_CERT)
.build();
protected final Type type;
protected PublicKey publicKey;
protected long serial;
protected String id;
protected Collection<String> principals;
protected List<OpenSshCertificate.CertificateOption> criticalOptions;
protected List<OpenSshCertificate.CertificateOption> extensions;
// match ssh-keygen behavior where the default would be forever
protected long validAfter = OpenSshCertificate.MIN_EPOCH;
// match ssh-keygen behavior where the default would be forever
protected long validBefore = OpenSshCertificate.INFINITY;
protected byte[] nonce;
protected OpenSshCertificateBuilder(Type type) {
super();
this.type = type;
}
public static OpenSshCertificateBuilder userCertificate() {
return new OpenSshCertificateBuilder(Type.USER);
}
public static OpenSshCertificateBuilder hostCertificate() {
return new OpenSshCertificateBuilder(Type.HOST);
}
public OpenSshCertificateBuilder publicKey(PublicKey publicKey) {
this.publicKey = publicKey;
return this;
}
public OpenSshCertificateBuilder serial(long serial) {
this.serial = serial;
return this;
}
public OpenSshCertificateBuilder id(String id) {
this.id = id;
return this;
}
public OpenSshCertificateBuilder principals(Collection<String> principals) {
this.principals = principals;
return this;
}
public OpenSshCertificateBuilder criticalOptions(List<OpenSshCertificate.CertificateOption> criticalOptions) {
this.criticalOptions = criticalOptions;
return this;
}
public OpenSshCertificateBuilder extensions(List<OpenSshCertificate.CertificateOption> extensions) {
this.extensions = extensions;
return this;
}
public OpenSshCertificateBuilder validAfter(long validAfter) {
this.validAfter = validAfter;
return this;
}
public OpenSshCertificateBuilder nonce(byte[] nonce) {
this.nonce = nonce;
return this;
}
/**
* If null, uses {@link OpenSshCertificate#MIN_EPOCH}
*
* @param validAfter {@link Instant} to use for validBefore
* @return Self reference
*/
public OpenSshCertificateBuilder validAfter(Instant validAfter) {
if (validAfter == null) {
return validAfter(OpenSshCertificate.MIN_EPOCH);
} else if (Instant.EPOCH.compareTo(validAfter) <= 0) {
return validAfter(validAfter.getEpochSecond());
}
throw new IllegalArgumentException("Valid-after cannot be < epoch");
}
public OpenSshCertificateBuilder validBefore(long validBefore) {
this.validBefore = validBefore;
return this;
}
/**
* If null, uses {@link OpenSshCertificate#INFINITY}
*
* @param validBefore {@link Instant} to use for validBefore
* @return Self reference
*/
public OpenSshCertificateBuilder validBefore(Instant validBefore) {
if (validBefore == null) {
return validBefore(OpenSshCertificate.INFINITY);
} else if (Instant.EPOCH.compareTo(validBefore) <= 0) {
return validBefore(validBefore.getEpochSecond());
}
throw new IllegalArgumentException("Valid-before cannot be < epoch");
}
protected void validate() {
// nonce should be 16 or 32 bytes according to
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys#L151-L153
if (nonce != null && (nonce.length != 16 && nonce.length != 32)) {
throw new IllegalStateException("'nonce' must be 16 or 32 bytes");
}
if (type == null) {
throw new IllegalStateException("'type' is required");
}
if (id == null) {
throw new IllegalStateException("'id' is required");
}
if (publicKey == null) {
throw new IllegalStateException("'publicKey' is required");
}
}
/**
* Creates a certificate signed with the given CA key. For RSA keys "rsa-sha2-512" is used for the signature.
*
* @param caKeypair CA key used to sign
* @return the signed certificate
* @throws Exception if an error occurred
*/
public OpenSshCertificate sign(KeyPair caKeypair) throws Exception {
return sign(caKeypair, null);
}
/**
* Creates a certificate signed with the given CA key using the specified signature algorithm. If a signature
* algorithm is given, it must be appropriate for the CA key type, otherwise an exception is thrown. If
* {@code signatureAlgorithm == null}, an appropriate signature algorithm is chosen automatically, for RSA keys
* "rsa-sha2-512" is used then.
*
* @param caKeypair CA key used to sign
* @param signatureAlgorithm to use; if {@code null} automatically chosen based on the CA key type
* @return the signed certificate
* @throws Exception if an error occurred
*/
public OpenSshCertificate sign(KeyPair caKeypair, String signatureAlgorithm) throws Exception {
validate();
final String publicKeyType = KeyUtils.getKeyType(publicKey);
final String certType = SIGNATURE_ALGORITHM_MAP.get(publicKeyType);
// only certain kind of keys can be OpenSSH Certificates
if (certType == null) {
throw new UnsupportedOperationException(
"unsupported public key type '" + publicKeyType + "' for OpenSSH Certificate");
}
final OpenSshCertificateImpl cert = new OpenSshCertificateImpl();
cert.setKeyType(certType);
cert.setType(type);
cert.setCertPubKey(publicKey);
cert.setSerial(serial);
cert.setId(id);
if (principals != null && !principals.isEmpty()) {
cert.setPrincipals(new ArrayList<>(principals));
}
if (criticalOptions != null && !criticalOptions.isEmpty()) {
cert.setCriticalOptions(new ArrayList<>(criticalOptions));
}
if (extensions != null && !extensions.isEmpty()) {
cert.setExtensions(new ArrayList<>(extensions));
}
cert.setValidAfter(validAfter);
cert.setValidBefore(validBefore);
cert.setCaPubKey(caKeypair.getPublic());
if (nonce != null) {
cert.setNonce(nonce);
} else {
SecureRandom rand = new SecureRandom();
byte[] tempNonce = new byte[32];
rand.nextBytes(tempNonce);
cert.setNonce(tempNonce);
}
String algo = KeyUtils.getKeyType(caKeypair.getPublic());
NamedFactory<? extends Signature> factory;
if (signatureAlgorithm != null) {
ValidateUtils.checkTrue(KeyUtils.getAllEquivalentKeyTypes(algo).contains(signatureAlgorithm),
"Invalid CA signature algorithm %s for CA key type %s", signatureAlgorithm, algo);
algo = signatureAlgorithm;
factory = BuiltinSignatures.fromFactoryName(algo);
} else {
factory = SignatureFactory.resolveSignatureFactory(algo, BaseBuilder.DEFAULT_SIGNATURE_PREFERENCE);
}
Signature signer = factory == null ? null : factory.create();
ValidateUtils.checkNotNull(signer, "No signer could be located for signature algorithm=%s", algo);
final ByteArrayBuffer toBeSignedBuf = new ByteArrayBuffer();
toBeSignedBuf.putRawPublicKey(cert);
final byte[] toSign = toBeSignedBuf.getCompactData();
signer.initSigner(null, caKeypair.getPrivate());
signer.update(null, toSign);
final ByteArrayBuffer tmpBuffer = new ByteArrayBuffer();
tmpBuffer.putString(factory.getName());
tmpBuffer.putBytes(signer.sign(null));
cert.setMessage(toSign);
cert.setSignature(tmpBuffer.getCompactData());
return cert;
}
}