import static java.util.Objects.requireNonNull;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
-import java.io.IOException;
-import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.Provider;
-import java.security.Security;
-import java.util.Base64;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.openssl.PEMEncryptedKeyPair;
-import org.bouncycastle.openssl.PEMKeyPair;
-import org.bouncycastle.openssl.PEMParser;
-import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
-import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.opendaylight.aaa.encrypt.AAAEncryptionService;
import org.opendaylight.netconf.client.conf.NetconfClientConfiguration.NetconfClientProtocol;
import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder;
@Component
@Singleton
public final class NetconfClientConfigurationBuilderFactoryImpl implements NetconfClientConfigurationBuilderFactory {
- private static final Provider BCPROV;
-
- static {
- final var prov = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
- BCPROV = prov != null ? prov : new BouncyCastleProvider();
- }
-
private final SslHandlerFactoryProvider sslHandlerFactoryProvider;
private final AAAEncryptionService encryptionService;
private final CredentialProvider credentialProvider;
@Override
public NetconfClientConfigurationBuilder createClientConfigurationBuilder(final NodeId nodeId,
- final NetconfNode node) {
+ final NetconfNode node) {
final var builder = NetconfClientConfigurationBuilder.create();
final var protocol = node.getProtocol();
if (node.requireTcpOnly()) {
final var keyBased = keyAuth.getKeyBased();
sshParamsBuilder.setClientIdentity(new ClientIdentityBuilder().setUsername(keyBased.getUsername()).build());
confBuilder.withSshConfigurator(factoryMgr -> {
- final var keyPair = getKeyPair(keyBased.getKeyId());
+ final var keyId = keyBased.getKeyId();
+ final var keyPair = credentialProvider.credentialForId(keyId);
+ if (keyPair == null) {
+ throw new IllegalArgumentException("No keypair found with keyId=" + keyId);
+ }
factoryMgr.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keyPair));
final var factory = new UserAuthPublicKeyFactory();
factory.setSignatureFactories(factoryMgr.getSignatureFactories());
.build())
.build();
}
-
- private KeyPair getKeyPair(final String keyId) {
- // public key retrieval logic taken from DatastoreBackedPublicKeyAuth
- final var dsKeypair = credentialProvider.credentialForId(keyId);
- if (dsKeypair == null) {
- throw new IllegalArgumentException("No keypair found with keyId=" + keyId);
- }
- final var passPhrase = Strings.isNullOrEmpty(dsKeypair.getPassphrase()) ? "" : dsKeypair.getPassphrase();
- try {
- return decodePrivateKey(decryptString(dsKeypair.getPrivateKey()), decryptString(passPhrase));
- } catch (IOException e) {
- throw new IllegalStateException("Could not decode private key with keyId=" + keyId, e);
- }
- }
-
- private String decryptString(final String encrypted) {
- final byte[] cryptobytes = Base64.getDecoder().decode(encrypted);
- final byte[] clearbytes;
- try {
- clearbytes = encryptionService.decrypt(cryptobytes);
- } catch (GeneralSecurityException e) {
- throw new IllegalStateException("Failed to decrypt", e);
- }
- return new String(clearbytes, StandardCharsets.UTF_8);
- }
-
-
- @VisibleForTesting
- static KeyPair decodePrivateKey(final String privateKey, final String passphrase) throws IOException {
- try (var keyReader = new PEMParser(new StringReader(privateKey.replace("\\n", "\n")))) {
- final var obj = keyReader.readObject();
-
- final PEMKeyPair keyPair;
- if (obj instanceof PEMEncryptedKeyPair encrypted) {
- keyPair = encrypted.decryptKeyPair(new JcePEMDecryptorProviderBuilder()
- .setProvider(BCPROV)
- .build(passphrase.toCharArray()));
- } else if (obj instanceof PEMKeyPair plain) {
- keyPair = plain;
- } else {
- throw new IllegalArgumentException("Unhandled private key " + obj.getClass());
- }
-
- return new JcaPEMKeyConverter().getKeyPair(keyPair);
- }
- }
}
<scope>provided</scope>
<optional>true</optional>
</dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk18on</artifactId>
+ </dependency>
<dependency>
<groupId>org.opendaylight.aaa</groupId>
<artifactId>aaa-encrypt-service</artifactId>
*/
package org.opendaylight.netconf.keystore.legacy;
+import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
@NonNullByDefault
public record NetconfKeystore(
Map<String, CertifiedPrivateKey> privateKeys,
- Map<String, X509Certificate> trustedCertificates) implements Immutable {
- public static final NetconfKeystore EMPTY = new NetconfKeystore(Map.of(), Map.of());
+ Map<String, X509Certificate> trustedCertificates,
+ Map<String, KeyPair> credentials) implements Immutable {
+ public static final NetconfKeystore EMPTY = new NetconfKeystore(Map.of(), Map.of(), Map.of());
public NetconfKeystore {
privateKeys = Map.copyOf(privateKeys);
trustedCertificates = Map.copyOf(trustedCertificates);
+ credentials = Map.copyOf(credentials);
}
}
import org.opendaylight.netconf.keystore.legacy.impl.DefaultNetconfKeystoreService.ConfigStateBuilder;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.keystore.entry.KeyCredential;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
}
}
}
+ for (var mod : rootNode.getModifiedChildren(KeyCredential.class)) {
+ switch (mod.modificationType()) {
+ case SUBTREE_MODIFIED, WRITE -> {
+ final var keyCredential = mod.dataAfter();
+ builder.credentials().put(keyCredential.requireKeyId(), keyCredential);
+ }
+ case DELETE -> builder.credentials().remove(mod.dataBefore().requireKeyId());
+ default -> {
+ // no-op
+ }
+ }
+ }
}
}
}
\ No newline at end of file
package org.opendaylight.netconf.keystore.legacy.impl;
import static java.util.Objects.requireNonNull;
+import static java.util.Objects.requireNonNullElse;
import com.google.common.collect.Maps;
+import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
+import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import org.opendaylight.netconf.keystore.legacy.NetconfKeystoreService;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.keystore.entry.KeyCredential;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
import org.opendaylight.yangtools.concepts.AbstractObjectRegistration;
import org.opendaylight.yangtools.concepts.Immutable;
@NonNullByDefault
private record ConfigState(
Map<String, PrivateKey> privateKeys,
- Map<String, TrustedCertificate> trustedCertificates) implements Immutable {
- static final ConfigState EMPTY = new ConfigState(Map.of(), Map.of());
+ Map<String, TrustedCertificate> trustedCertificates,
+ Map<String, KeyCredential> credentials) implements Immutable {
+ static final ConfigState EMPTY = new ConfigState(Map.of(), Map.of(), Map.of());
ConfigState {
privateKeys = Map.copyOf(privateKeys);
trustedCertificates = Map.copyOf(trustedCertificates);
+ credentials = Map.copyOf(credentials);
}
}
@NonNullByDefault
record ConfigStateBuilder(
HashMap<String, PrivateKey> privateKeys,
- HashMap<String, TrustedCertificate> trustedCertificates) implements Mutable {
+ HashMap<String, TrustedCertificate> trustedCertificates,
+ HashMap<String, KeyCredential> credentials) implements Mutable {
ConfigStateBuilder {
requireNonNull(privateKeys);
requireNonNull(trustedCertificates);
+ requireNonNull(credentials);
}
}
private final AtomicReference<NetconfKeystore> keystore = new AtomicReference<>(null);
private final AtomicReference<ConfigState> config = new AtomicReference<>(ConfigState.EMPTY);
private final SecurityHelper securityHelper = new SecurityHelper();
+ private final AAAEncryptionService encryptionService;
private final Registration configListener;
private final Registration rpcSingleton;
@Reference final RpcProviderService rpcProvider,
@Reference final ClusterSingletonServiceProvider cssProvider,
@Reference final AAAEncryptionService encryptionService) {
+ this.encryptionService = requireNonNull(encryptionService);
configListener = dataBroker.registerTreeChangeListener(
DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
new ConfigListener(this));
final var prevState = config.getAcquire();
final var builder = new ConfigStateBuilder(new HashMap<>(prevState.privateKeys),
- new HashMap<>(prevState.trustedCertificates));
+ new HashMap<>(prevState.trustedCertificates), new HashMap<>(prevState.credentials));
task.accept(builder);
- final var newState = new ConfigState(builder.privateKeys, builder.trustedCertificates);
+ final var newState = new ConfigState(builder.privateKeys, builder.trustedCertificates, builder.credentials);
// Careful application -- check if listener is still up and whether the state was not updated.
if (configListener == null || config.compareAndExchangeRelease(prevState, newState) != prevState) {
certs.put(certName, x509cert);
}
+ final var creds = Maps.<String, KeyPair>newHashMapWithExpectedSize(newState.credentials.size());
+ for (var cred : newState.credentials.values()) {
+ final var keyId = cred.requireKeyId();
+ final String passPhrase;
+ try {
+ passPhrase = decryptString(requireNonNullElse(cred.getPassphrase(), ""));
+ } catch (GeneralSecurityException e) {
+ LOG.debug("Failed to decrypt pass phrase for {}", keyId, e);
+ failure = updateFailure(failure, e);
+ continue;
+ }
+
+ final String privateKey;
+ try {
+ privateKey = decryptString(cred.getPrivateKey());
+ } catch (GeneralSecurityException e) {
+ LOG.debug("Failed to decrypt private key for {}", keyId, e);
+ failure = updateFailure(failure, e);
+ continue;
+ }
+
+ final KeyPair keyPair;
+ try {
+ keyPair = securityHelper.decodePrivateKey(privateKey, passPhrase);
+ } catch (IOException e) {
+ LOG.debug("Failed to decode key pair for {}", keyId, e);
+ failure = updateFailure(failure, e);
+ continue;
+ }
+
+ creds.put(keyId, keyPair);
+ }
+
if (failure != null) {
LOG.warn("New configuration is invalid, not applying it", failure);
return;
}
- final var newKeystore = new NetconfKeystore(keys, certs);
+ final var newKeystore = new NetconfKeystore(keys, certs, creds);
keystore.setRelease(newKeystore);
consumers.forEach(consumer -> consumer.getInstance().accept(newKeystore));
}
private static byte[] base64Decode(final String base64) {
- return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
+ return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.UTF_8));
+ }
+
+ private String decryptString(final String encrypted) throws GeneralSecurityException {
+ return new String(encryptionService.decrypt(Base64.getDecoder().decode(encrypted)), StandardCharsets.UTF_8);
}
private static @NonNull Throwable updateFailure(final @Nullable Throwable failure, final @NonNull Exception ex) {
package org.opendaylight.netconf.keystore.legacy.impl;
import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringReader;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
+import java.security.KeyPair;
import java.security.PrivateKey;
+import java.security.Provider;
+import java.security.Security;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PEMEncryptedKeyPair;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.eclipse.jdt.annotation.NonNull;
final class SecurityHelper {
private CertificateFactory certFactory;
private KeyFactory dsaFactory;
private KeyFactory rsaFactory;
+ private Provider bcProv;
@NonNull PrivateKey generatePrivateKey(final byte[] privateKey) throws GeneralSecurityException {
final var keySpec = new PKCS8EncodedKeySpec(privateKey);
}
return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificate));
}
+
+ @NonNull KeyPair decodePrivateKey(final String privateKey, final String passphrase) throws IOException {
+ if (bcProv == null) {
+ final var prov = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
+ bcProv = prov != null ? prov : new BouncyCastleProvider();
+ }
+
+ try (var keyReader = new PEMParser(new StringReader(privateKey.replace("\\n", "\n")))) {
+ final var obj = keyReader.readObject();
+
+ final PEMKeyPair keyPair;
+ if (obj instanceof PEMEncryptedKeyPair encrypted) {
+ keyPair = encrypted.decryptKeyPair(new JcePEMDecryptorProviderBuilder()
+ .setProvider(bcProv)
+ .build(passphrase.toCharArray()));
+ } else if (obj instanceof PEMKeyPair plain) {
+ keyPair = plain;
+ } else {
+ throw new IOException("Unhandled private key " + obj.getClass());
+ }
+
+ return new JcaPEMKeyConverter().getKeyPair(keyPair);
+ }
+ }
}
\ No newline at end of file
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.topology.spi;
+package org.opendaylight.netconf.keystore.legacy.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.bouncycastle.openssl.EncryptionException;
import org.junit.jupiter.api.Test;
-class PKIUtilTest {
+class SecurityHelperTest {
+ private final SecurityHelper helper = new SecurityHelper();
+
@Test
void testRSAKey() throws Exception {
assertNotNull(decodePrivateKey("rsa", ""));
assertEquals("exception using cipher - please check password and data.", ex.getMessage());
}
- private static KeyPair decodePrivateKey(final String resourceName, final String password) throws Exception {
- return NetconfClientConfigurationBuilderFactoryImpl.decodePrivateKey(
- new String(PKIUtilTest.class.getResourceAsStream("/pki/" + resourceName).readAllBytes(),
+ private KeyPair decodePrivateKey(final String resourceName, final String password) throws Exception {
+ return helper.decodePrivateKey(
+ new String(SecurityHelperTest.class.getResourceAsStream("/pki/" + resourceName).readAllBytes(),
StandardCharsets.UTF_8),
password);
}
*/
package org.opendaylight.netconf.client.mdsal.api;
+import java.security.KeyPair;
import org.eclipse.jdt.annotation.Nullable;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.keystore.entry.KeyCredential;
public interface CredentialProvider {
/**
- * Get the a {@link KeyCredential} for a particular id.
+ * Get the a {@link KeyPair} for a particular id.
*
* @param id Credential id
- * @return A {@link KeyCredential} object, {@code null} if not found
+ * @return A {@link KeyPair} object, {@code null} if not found
* @throws NullPointerException if {@code id} is {@code null}
*/
- @Nullable KeyCredential credentialForId(String id);
+ @Nullable KeyPair credentialForId(String id);
}
*/
package org.opendaylight.netconf.client.mdsal.impl;
+import static java.util.Objects.requireNonNull;
+
+import java.security.KeyPair;
import java.util.Map;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.jdt.annotation.NonNull;
-import org.opendaylight.mdsal.binding.api.DataBroker;
-import org.opendaylight.mdsal.binding.api.DataListener;
-import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
-import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.netconf.client.mdsal.api.CredentialProvider;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.keystore.entry.KeyCredential;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.keystore.entry.KeyCredentialKey;
+import org.opendaylight.netconf.keystore.legacy.NetconfKeystoreService;
import org.opendaylight.yangtools.concepts.Registration;
-import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@Singleton
@Component(service = CredentialProvider.class)
-public final class DefaultCredentialProvider implements CredentialProvider, DataListener<Keystore>, AutoCloseable {
- private static final Logger LOG = LoggerFactory.getLogger(DefaultCredentialProvider.class);
-
+public final class DefaultCredentialProvider implements CredentialProvider, AutoCloseable {
private final @NonNull Registration reg;
- private volatile @NonNull Map<KeyCredentialKey, KeyCredential> credentials = Map.of();
+ private volatile @NonNull Map<String, KeyPair> credentials = Map.of();
@Inject
@Activate
- public DefaultCredentialProvider(@Reference final DataBroker dataBroker) {
- reg = dataBroker.registerDataListener(
- DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)), this);
+ public DefaultCredentialProvider(@Reference final NetconfKeystoreService keystoreService) {
+ reg = keystoreService.registerKeystoreConsumer(keystore -> {
+ credentials = keystore.credentials();
+ });
}
@Deactivate
}
@Override
- public KeyCredential credentialForId(final String id) {
- return credentials.get(new KeyCredentialKey(id));
- }
-
- @Override
- public void dataChangedTo(final Keystore data) {
- final var newCredentials = data != null ? data.nonnullKeyCredential()
- : Map.<KeyCredentialKey, KeyCredential>of();
- LOG.debug("Updating to {} credentials", newCredentials.size());
- credentials = newCredentials;
+ public KeyPair credentialForId(final String id) {
+ return credentials.get(requireNonNull(id));
}
}