This is an automated email from the ASF dual-hosted git repository.

lahirujayathilake pushed a commit to branch custos-signer
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git


The following commit(s) were added to refs/heads/custos-signer by this push:
     new 4c34533f3 support key generation using the types RSA and ECDSA
4c34533f3 is described below

commit 4c34533f35e7f16ed1e11669b3fd0f94c65e0f13
Author: lahiruj <[email protected]>
AuthorDate: Tue Dec 2 23:32:14 2025 -0500

    support key generation using the types RSA and ECDSA
---
 .../org/apache/custos/signer/sdk/SshClient.java    |  25 ++-
 .../custos/signer/sdk/config/SdkConfiguration.java |  26 +++
 .../apache/custos/signer/sdk/util/SshKeyUtils.java | 194 +++++++++++++--------
 .../signer/sdk/SshClientIntegrationTest.java       | 107 ++++++++++++
 .../signer/sdk/config/SdkConfigurationTest.java    |  50 ++++++
 .../custos/signer/sdk/util/SshKeyUtilsTest.java    |  94 +++++-----
 .../signer/service/ca/SshCertificateSigner.java    | 137 +++++++++++++--
 .../custos/signer/service/policy/KeyType.java      |  48 +++++
 .../signer/service/policy/PolicyEnforcer.java      |   7 +-
 9 files changed, 536 insertions(+), 152 deletions(-)

diff --git 
a/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/SshClient.java
 
b/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/SshClient.java
index 6b1881218..2e21956fe 100644
--- 
a/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/SshClient.java
+++ 
b/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/SshClient.java
@@ -71,18 +71,31 @@ public class SshClient implements AutoCloseable {
      * @throws SshClientException if certificate request fails
      */
     public CertificateMaterials requestCertificateMaterials(String 
clientAlias, String principal, int ttlSeconds, String userToken) {
+        return requestCertificateMaterials(clientAlias, principal, ttlSeconds, 
userToken, null);
+    }
+
+    /**
+     * Request certificate materials specifying key type.
+     *
+     * @param clientAlias Alias configured in SDK configuration
+     * @param principal   SSH username
+     * @param ttlSeconds  TTL in seconds
+     * @param userToken   OIDC user access token
+     * @param keyType     Key type ("ed25519", "rsa", "ecdsa")
+     * @return CertificateMaterials
+     */
+    public CertificateMaterials requestCertificateMaterials(String 
clientAlias, String principal, int ttlSeconds, String userToken, String 
keyType) {
         try {
-            logger.debug("Requesting certificate materials for client: {}, 
principal: {}, ttl: {}s", clientAlias, principal, ttlSeconds);
+            logger.debug("Requesting certificate materials for client: {}, 
principal: {}, ttl: {}s, keyType: {}", clientAlias, principal, ttlSeconds, 
keyType);
 
             // Resolve client alias to configuration
             SdkConfiguration.ClientConfig clientConfig = 
configuration.getClientConfig(clientAlias)
                     .orElseThrow(() -> new IllegalArgumentException("Client 
alias not found: " + clientAlias));
 
-            // TODO: Support RSA and ECDSA key types in addition to Ed25519
-            // Generate ephemeral Ed25519 keypair
-            KeyPair keyPair = SshKeyUtils.generateEd25519KeyPair();
+            String defaultKeyType = clientConfig.getKeyType() == null ? 
"ed25519" : clientConfig.getKeyType();
+            String normalizedKeyType = keyType == null ? 
defaultKeyType.toLowerCase() : keyType.toLowerCase();
+            KeyPair keyPair = SshKeyUtils.generateKeyPair(normalizedKeyType);
 
-            // Convert public key to OpenSSH format
             String publicKeyOpenSsh = 
SshKeyUtils.keyPairToOpenSshPublicKey(keyPair);
             byte[] publicKeyBytes = 
publicKeyOpenSsh.getBytes(StandardCharsets.UTF_8);
 
@@ -104,7 +117,7 @@ public class SshClient implements AutoCloseable {
             // Convert certificate bytes to OpenSSH cert string format
             // Use principal as comment
             String opensshCert = SshKeyUtils.certBytesToOpenSshCertString(
-                    certResponse.getCertificate(), principal);
+                    certResponse.getCertificate(), normalizedKeyType, 
principal);
 
             // Create defensive copy of certBytes
             byte[] certBytesCopy = certResponse.getCertificate().clone();
diff --git 
a/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/config/SdkConfiguration.java
 
b/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/config/SdkConfiguration.java
index 6548f4dcd..29698526c 100644
--- 
a/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/config/SdkConfiguration.java
+++ 
b/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/config/SdkConfiguration.java
@@ -19,6 +19,7 @@
 package org.apache.custos.signer.sdk.config;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.custos.signer.service.policy.KeyType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -327,6 +328,9 @@ public class SdkConfiguration {
         @JsonProperty("client-secret")
         private String clientSecret;
 
+        @JsonProperty("key-type")
+        private String keyType = KeyType.ED25519.id();
+
         public ClientConfig() {
         }
 
@@ -336,6 +340,15 @@ public class SdkConfiguration {
             this.clientSecret = clientSecret;
         }
 
+        public ClientConfig(String alias, String clientId, String 
clientSecret, String keyType) {
+            this.alias = alias;
+            this.clientId = clientId;
+            this.clientSecret = clientSecret;
+            if (keyType != null && !keyType.isEmpty()) {
+                this.keyType = keyType.toLowerCase();
+            }
+        }
+
         public String getAlias() {
             return alias;
         }
@@ -359,6 +372,14 @@ public class SdkConfiguration {
         public void setClientSecret(String clientSecret) {
             this.clientSecret = clientSecret;
         }
+
+        public String getKeyType() {
+            return keyType;
+        }
+
+        public void setKeyType(String keyType) {
+            this.keyType = keyType;
+        }
     }
 
     /**
@@ -425,6 +446,11 @@ public class SdkConfiguration {
             return this;
         }
 
+        public Builder addClient(String alias, String clientId, String 
clientSecret, String keyType) {
+            clients.put(alias, new ClientConfig(alias, clientId, clientSecret, 
keyType));
+            return this;
+        }
+
         public SdkConfiguration build() {
             sdkConfig.setSigner(signerConfig);
             sdkConfig.setKeyStore(keyStoreConfig);
diff --git 
a/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/util/SshKeyUtils.java
 
b/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/util/SshKeyUtils.java
index 5ebd43a7f..00f4d09f8 100644
--- 
a/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/util/SshKeyUtils.java
+++ 
b/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/util/SshKeyUtils.java
@@ -18,6 +18,7 @@
  */
 package org.apache.custos.signer.sdk.util;
 
+import org.apache.custos.signer.service.policy.KeyType;
 import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
 import org.bouncycastle.crypto.util.PrivateKeyFactory;
 import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
@@ -25,32 +26,44 @@ import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
 import java.io.StringWriter;
+import java.math.BigInteger;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
 import java.util.Base64;
 
 /**
  * Utility class for SSH key generation and format conversion.
- * TODO: Add support for RSA and ECDSA key types.
  */
 public final class SshKeyUtils {
 
-    // TODO: Support multiple key types (RSA, ECDSA) in addition to Ed25519
     private static final String SSH_KEY_TYPE_ED25519 = "ssh-ed25519";
     private static final String SSH_CERT_TYPE_ED25519 = 
"[email protected]";
 
     private SshKeyUtils() {
     }
 
-    /**
-     * Generate a new Ed25519 keypair.
-     *
-     * @return Generated Ed25519 KeyPair
-     * @throws Exception if key generation fails
-     */
-    public static KeyPair generateEd25519KeyPair() throws Exception {
-        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
-        return keyGen.generateKeyPair();
+    public static KeyPair generateKeyPair(String keyType) throws Exception {
+        KeyType normalized = KeyType.from(keyType);
+        switch (normalized) {
+            case ED25519:
+                return 
KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
+            case RSA:
+                KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA");
+                rsaGen.initialize(2048);
+                return rsaGen.generateKeyPair();
+            case ECDSA:
+                KeyPairGenerator ecGen = KeyPairGenerator.getInstance("EC");
+                ecGen.initialize(new ECGenParameterSpec("secp256r1"));
+                return ecGen.generateKeyPair();
+            default:
+                throw new IllegalArgumentException("Unsupported key type: " + 
keyType);
+        }
     }
 
     /**
@@ -217,66 +230,65 @@ public final class SshKeyUtils {
         out.write(data);
     }
 
-    /**
-     * Convert a public key to OpenSSH format.
-     * <p>
-     * Format: "ssh-ed25519 &lt;base64-encoded-ssh-wire-blob&gt; 
&lt;comment&gt;"
-     * <p>
-     * The SSH wire format blob contains:
-     * - 4 bytes: length of key type string ("ssh-ed25519" = 11 bytes)
-     * - key type string bytes ("ssh-ed25519")
-     * - 4 bytes: length of public key (32 bytes)
-     * - 32 bytes: public key
-     * <p>
-     * This entire blob is then base64-encoded to produce the OpenSSH public 
key format.
-     * <p>
-     * TODO: Support RSA and ECDSA key types (detect key type and format 
accordingly).
-     *
-     * @param keyPair KeyPair containing the public key
-     * @return OpenSSH-formatted public key string
-     * @throws Exception if conversion fails
-     */
     public static String keyPairToOpenSshPublicKey(KeyPair keyPair) throws 
Exception {
         if (keyPair == null || keyPair.getPublic() == null) {
             throw new IllegalArgumentException("KeyPair or public key is 
null");
         }
 
-        // Extract Ed25519 public key bytes (32 bytes)
-        // The encoded format for Ed25519 is: OID (12 bytes) + public key (32 
bytes) = 44 bytes
-        byte[] encoded = keyPair.getPublic().getEncoded();
-        if (encoded.length < 44) {
-            throw new IllegalArgumentException("Invalid Ed25519 public key 
encoding");
-        }
-
-        // Extract the 32-byte public key (last 32 bytes)
-        byte[] publicKeyBytes = new byte[32];
-        System.arraycopy(encoded, encoded.length - 32, publicKeyBytes, 0, 32);
-
-        // SSH wire format: [4-byte length][key-type-string][4-byte 
length][32-byte public key]
-        java.io.ByteArrayOutputStream blobStream = new 
java.io.ByteArrayOutputStream();
-        java.io.DataOutputStream blob = new 
java.io.DataOutputStream(blobStream);
-
-        try {
-            // Write key type string length (4 bytes, big-endian)
-            byte[] keyTypeBytes = 
SSH_KEY_TYPE_ED25519.getBytes(java.nio.charset.StandardCharsets.UTF_8);
-            blob.writeInt(keyTypeBytes.length);
-
-            blob.write(keyTypeBytes);
+        PublicKey publicKey = keyPair.getPublic();
+        String algorithm = publicKey.getAlgorithm();
 
-            blob.writeInt(publicKeyBytes.length);
-
-            blob.write(publicKeyBytes);
-
-            blob.flush();
-        } finally {
-            blob.close();
+        if ("EdDSA".equals(algorithm) || "Ed25519".equals(algorithm)) {
+            byte[] encoded = publicKey.getEncoded();
+            if (encoded.length < 44) {
+                throw new IllegalArgumentException("Invalid Ed25519 public key 
encoding");
+            }
+            byte[] publicKeyBytes = new byte[32];
+            System.arraycopy(encoded, encoded.length - 32, publicKeyBytes, 0, 
32);
+
+            ByteArrayOutputStream blobStream = new ByteArrayOutputStream();
+            try (DataOutputStream blob = new DataOutputStream(blobStream)) {
+                writeString(blob, 
SSH_KEY_TYPE_ED25519.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+                blob.writeInt(publicKeyBytes.length);
+                blob.write(publicKeyBytes);
+                blob.flush();
+            }
+
+            String base64Key = 
Base64.getEncoder().encodeToString(blobStream.toByteArray());
+            return SSH_KEY_TYPE_ED25519 + " " + base64Key + " 
custos-generated";
+
+        } else if ("RSA".equalsIgnoreCase(algorithm)) {
+            RSAPublicKey rsa = (RSAPublicKey) publicKey;
+            ByteArrayOutputStream blobStream = new ByteArrayOutputStream();
+            try (DataOutputStream blob = new DataOutputStream(blobStream)) {
+                writeString(blob, 
"ssh-rsa".getBytes(java.nio.charset.StandardCharsets.UTF_8));
+                writeMpInt(blob, rsa.getPublicExponent());
+                writeMpInt(blob, rsa.getModulus());
+                blob.flush();
+            }
+
+            String base64Key = 
Base64.getEncoder().encodeToString(blobStream.toByteArray());
+            return "ssh-rsa " + base64Key + " custos-generated";
+
+        } else if ("EC".equalsIgnoreCase(algorithm)) {
+            ECPublicKey ecKey = (ECPublicKey) publicKey;
+            String curveName = "nistp256";
+            ECPoint w = ecKey.getW();
+            byte[] q = encodeEcPointUncompressed(w, ecKey.getParams());
+
+            ByteArrayOutputStream blobStream = new ByteArrayOutputStream();
+            try (DataOutputStream blob = new DataOutputStream(blobStream)) {
+                writeString(blob, "ecdsa-sha2-" + curveName);
+                writeString(blob, curveName);
+                writeString(blob, q);
+                blob.flush();
+            }
+
+            String base64Key = 
Base64.getEncoder().encodeToString(blobStream.toByteArray());
+            return "ecdsa-sha2-" + curveName + " " + base64Key + " 
custos-generated";
+        } else {
+            throw new IllegalArgumentException("Unsupported public key 
algorithm: " + algorithm);
         }
-
-        // Encode entire blob to base64
-        String base64Key = 
Base64.getEncoder().encodeToString(blobStream.toByteArray());
-
-        // Format: "ssh-ed25519 <base64-ssh-wire-blob> <comment>"
-        return SSH_KEY_TYPE_ED25519 + " " + base64Key + " custos-generated";
     }
 
     /**
@@ -288,27 +300,69 @@ public final class SshKeyUtils {
      * certificate type identifier. The comment typically contains the 
principal
      * or client alias for identification.
      * <p>
-     * TODO: Detect certificate type from bytes and use appropriate format 
([email protected], [email protected], etc.).
      *
      * @param certBytes Certificate bytes (raw binary certificate data)
      * @param comment   Comment to append (typically principal or clientAlias)
      * @return OpenSSH certificate string format
      * @throws Exception if conversion fails
      */
-    public static String certBytesToOpenSshCertString(byte[] certBytes, String 
comment) throws Exception {
+    public static String certBytesToOpenSshCertString(byte[] certBytes, String 
keyType, String comment) throws Exception {
         if (certBytes == null || certBytes.length == 0) {
             throw new IllegalArgumentException("Certificate bytes cannot be 
null or empty");
         }
 
-        // Encode certificate bytes to base64
-        String base64Cert = Base64.getEncoder().encodeToString(certBytes);
+        KeyType normalized = KeyType.from(keyType);
+        String certType = normalized == KeyType.RSA
+                ? "[email protected]"
+                : normalized == KeyType.ECDSA
+                ? "[email protected]"
+                : SSH_CERT_TYPE_ED25519;
 
-        // Format: "[email protected] <base64> <comment>"
-        String certString = SSH_CERT_TYPE_ED25519 + " " + base64Cert;
+        String base64Cert = Base64.getEncoder().encodeToString(certBytes);
+        String certString = certType + " " + base64Cert;
         if (comment != null && !comment.isEmpty()) {
             certString += " " + comment;
         }
 
         return certString;
     }
+
+    public static String certBytesToOpenSshCertString(byte[] certBytes, String 
comment) throws Exception {
+        return certBytesToOpenSshCertString(certBytes, "ed25519", comment);
+    }
+
+    private static void writeMpInt(DataOutputStream out, BigInteger value) 
throws Exception {
+        byte[] bytes = value.toByteArray();
+        writeString(out, bytes);
+    }
+
+    private static byte[] encodeEcPointUncompressed(ECPoint point, 
ECParameterSpec params) {
+        int fieldSize = (params.getCurve().getField().getFieldSize() + 7) / 8;
+        byte[] x = toFixedLength(point.getAffineX(), fieldSize);
+        byte[] y = toFixedLength(point.getAffineY(), fieldSize);
+        byte[] q = new byte[1 + x.length + y.length];
+        q[0] = 0x04;
+        System.arraycopy(x, 0, q, 1, x.length);
+        System.arraycopy(y, 0, q, 1 + x.length, y.length);
+        return q;
+    }
+
+    private static byte[] toFixedLength(BigInteger value, int length) {
+        byte[] bytes = value.toByteArray();
+        if (bytes.length == length) {
+            return bytes;
+        }
+        byte[] out = new byte[length];
+        if (bytes.length > length) {
+            System.arraycopy(bytes, bytes.length - length, out, 0, length);
+        } else {
+            System.arraycopy(bytes, 0, out, length - bytes.length, 
bytes.length);
+        }
+        return out;
+    }
+
+    private static void writeString(DataOutputStream out, String str) throws 
Exception {
+        byte[] data = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);
+        writeString(out, data);
+    }
 }
diff --git 
a/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/SshClientIntegrationTest.java
 
b/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/SshClientIntegrationTest.java
new file mode 100644
index 000000000..1bb6b70b2
--- /dev/null
+++ 
b/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/SshClientIntegrationTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 specific language
+ * governing permissions and limitations under the License.
+ */
+package org.apache.custos.signer.sdk;
+
+import org.apache.custos.signer.sdk.config.SdkConfiguration;
+import org.apache.custos.signer.sdk.keystore.InMemoryKeyStore;
+import org.apache.custos.signer.sdk.keystore.KeyStoreProvider;
+import org.apache.custos.signer.service.policy.KeyType;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Integration test for SSH client certificate request functionality.
+ */
+public class SshClientIntegrationTest {
+
+    @ParameterizedTest
+    @MethodSource("keyTypes")
+    void testRequestCertificateMaterials(String keyType) throws Exception {
+        SdkConfiguration config = new SdkConfiguration.Builder()
+                .tenantId("nexus")
+                .signerServiceAddress("localhost:9095")
+                .tlsEnabled(false)
+                .keyStoreBackend("in-memory")
+                .addClient("test-client", "test-client", "test-secret")
+                .build();
+
+        KeyStoreProvider keyStore = new InMemoryKeyStore();
+
+        SshClient client = new SshClient.Builder()
+                .configuration(config)
+                .keyStoreBackend(keyStore)
+                .build();
+
+        try {
+            System.out.println("Requesting certificate materials (this will 
auto-generate CA key if first time) using keyType=" + keyType);
+
+            CertificateMaterials materials = 
client.requestCertificateMaterials(
+                    "test-client",  // client alias
+                    "exouser",    // principal (SSH username)
+                    3600,          // TTL: 1 hour
+                    "test-token",  // user token (for now, just a placeholder 
TODO)
+                    keyType
+            );
+
+            assertNotNull(materials, "Certificate materials should not be 
null");
+            assertNotNull(materials.keyPair(), "KeyPair should not be null");
+            assertNotNull(materials.privateKeyPem(), "Private key PEM should 
not be null");
+            assertNotNull(materials.publicKeyOpenSsh(), "Public key OpenSSH 
should not be null");
+            assertNotNull(materials.opensshCert(), "OpenSSH cert should not be 
null");
+            assertNotNull(materials.certBytes(), "Certificate bytes should not 
be null");
+            assertTrue(Objects.requireNonNull(materials.certBytes()).length > 
0, "Certificate bytes should not be empty");
+
+            System.out.println("✓ Certificate materials received 
successfully!");
+            System.out.println("  Serial Number: " + materials.serial());
+            System.out.println("  CA Fingerprint: " + 
materials.caFingerprint());
+            System.out.println("  Target: " + materials.targetHost() + ":" + 
materials.targetPort());
+            System.out.println("  Valid After: " + materials.validAfter());
+            System.out.println("  Valid Before: " + materials.validBefore());
+
+            String keyFile = "/tmp/test-key-" + keyType;
+            String certFile = "/tmp/test-cert-" + keyType;
+            Files.write(Paths.get(keyFile), 
materials.privateKeyPem().getBytes());
+            String certContent = materials.opensshCert().trim();
+            Files.write(Paths.get(certFile), certContent.getBytes());
+
+            System.out.println("✓ Saved private key to: " + keyFile);
+            System.out.println("✓ Saved certificate to: " + certFile);
+            System.out.println("\nNext steps:");
+            System.out.println("1. Extract CA public key from Vault:");
+            System.out.println("   vault kv get -field=public_key 
ssh-ca/nexus/test-client/current > /tmp/ca.pub");
+            System.out.println("2. Configure HPC node to trust the CA (see 
Phase 4 in plan)");
+            System.out.println("3. Test SSH connection:");
+            System.out.println("   ssh -i " + keyFile + " -o CertificateFile=" 
+ certFile + " [email protected] -p 22");
+
+        } finally {
+            client.close();
+        }
+    }
+
+    private static Stream<String> keyTypes() {
+        return Stream.of(KeyType.ED25519.id(), KeyType.RSA.id(), 
KeyType.ECDSA.id());
+    }
+}
diff --git 
a/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/config/SdkConfigurationTest.java
 
b/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/config/SdkConfigurationTest.java
new file mode 100644
index 000000000..e0b37ba38
--- /dev/null
+++ 
b/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/config/SdkConfigurationTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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 specific language
+ * governing permissions and limitations under the License.
+ */
+package org.apache.custos.signer.sdk.config;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SdkConfigurationTest {
+
+    @Test
+    void defaultKeyTypeIsEd25519() {
+        SdkConfiguration config = new SdkConfiguration.Builder()
+                .tenantId("t1")
+                .signerServiceAddress("localhost:9095")
+                .addClient("a1", "c1", "s1")
+                .build();
+
+        SdkConfiguration.ClientConfig client = 
config.getClientConfig("a1").orElseThrow();
+        assertEquals("ed25519", client.getKeyType());
+    }
+
+    @Test
+    void keyTypeCanBeOverriddenPerClient() {
+        SdkConfiguration config = new SdkConfiguration.Builder()
+                .tenantId("t1")
+                .signerServiceAddress("localhost:9095")
+                .addClient("a1", "c1", "s1", "rsa")
+                .addClient("a2", "c2", "s2", "ecdsa")
+                .build();
+
+        assertEquals("rsa", 
config.getClientConfig("a1").orElseThrow().getKeyType());
+        assertEquals("ecdsa", 
config.getClientConfig("a2").orElseThrow().getKeyType());
+    }
+}
diff --git 
a/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/util/SshKeyUtilsTest.java
 
b/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/util/SshKeyUtilsTest.java
index 1d2cd65bd..b43486b59 100644
--- 
a/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/util/SshKeyUtilsTest.java
+++ 
b/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/util/SshKeyUtilsTest.java
@@ -18,15 +18,17 @@
  */
 package org.apache.custos.signer.sdk.util;
 
-import org.bouncycastle.openssl.PEMParser;
-import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.apache.custos.signer.service.policy.KeyType;
 import org.junit.jupiter.api.Test;
 
-import java.io.StringReader;
 import java.security.KeyPair;
 import java.util.Base64;
 
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Unit tests for SshKeyUtils
@@ -35,8 +37,8 @@ class SshKeyUtilsTest {
 
     @Test
     void testGenerateEd25519KeyPair() throws Exception {
-        KeyPair keyPair = SshKeyUtils.generateEd25519KeyPair();
-        
+        KeyPair keyPair = SshKeyUtils.generateKeyPair(KeyType.ED25519.id());
+
         assertNotNull(keyPair);
         assertNotNull(keyPair.getPrivate());
         assertNotNull(keyPair.getPublic());
@@ -46,54 +48,41 @@ class SshKeyUtilsTest {
 
     @Test
     void testKeyPairToPem() throws Exception {
-        KeyPair keyPair = SshKeyUtils.generateEd25519KeyPair();
+        KeyPair keyPair = SshKeyUtils.generateKeyPair(KeyType.ED25519.id());
         String pem = SshKeyUtils.keyPairToPem(keyPair);
-        
+
         assertNotNull(pem);
-        assertTrue(pem.contains("BEGIN") && pem.contains("PRIVATE KEY"));
-        assertTrue(pem.contains("END") && pem.contains("PRIVATE KEY"));
-        
-        // Verify round-trip: parse PEM back to PrivateKey using Bouncy Castle
-        try (PEMParser parser = new PEMParser(new StringReader(pem))) {
-            Object obj = parser.readObject();
-            assertNotNull(obj);
-            
-            JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
-            java.security.PrivateKey parsedKey;
-            if (obj instanceof org.bouncycastle.asn1.pkcs.PrivateKeyInfo) {
-                parsedKey = 
converter.getPrivateKey((org.bouncycastle.asn1.pkcs.PrivateKeyInfo) obj);
-            } else if (obj instanceof org.bouncycastle.openssl.PEMKeyPair) {
-                parsedKey = 
converter.getPrivateKey(((org.bouncycastle.openssl.PEMKeyPair) 
obj).getPrivateKeyInfo());
-            } else {
-                // Fallback: try to extract from key pair
-                org.bouncycastle.openssl.PEMKeyPair keyPairObj = 
(org.bouncycastle.openssl.PEMKeyPair) obj;
-                parsedKey = converter.getKeyPair(keyPairObj).getPrivate();
-            }
-            
-            assertNotNull(parsedKey);
-            // Java reports Ed25519 as "EdDSA"
-            assertEquals("EdDSA", parsedKey.getAlgorithm());
-        }
+        assertTrue(pem.contains("BEGIN OPENSSH PRIVATE KEY"));
+        assertTrue(pem.contains("END OPENSSH PRIVATE KEY"));
+
+        // Verify OpenSSH header and basic structure
+        String base64 = pem.lines()
+                .filter(l -> !l.startsWith("-----"))
+                .reduce("", (a, b) -> a + b);
+        byte[] decoded = Base64.getDecoder().decode(base64);
+        assertNotNull(decoded);
+        assertTrue(new String(decoded, 0, "openssh-key-v1\0".length(), 
java.nio.charset.StandardCharsets.UTF_8)
+                .startsWith("openssh-key-v1\0"));
     }
 
     @Test
     void testKeyPairToOpenSshPublicKey() throws Exception {
-        KeyPair keyPair = SshKeyUtils.generateEd25519KeyPair();
+        KeyPair keyPair = SshKeyUtils.generateKeyPair(KeyType.ED25519.id());
         String opensshKey = SshKeyUtils.keyPairToOpenSshPublicKey(keyPair);
-        
+
         assertNotNull(opensshKey);
         assertTrue(opensshKey.startsWith("ssh-ed25519 "));
-        
+
         // Verify format: "ssh-ed25519 <base64-ssh-wire-blob> <comment>"
         String[] parts = opensshKey.split("\\s+");
         assertEquals(3, parts.length);
         assertEquals("ssh-ed25519", parts[0]);
-        
+
         // Decode the SSH wire format blob
         byte[] wireBlob = Base64.getDecoder().decode(parts[1]);
         assertNotNull(wireBlob);
         assertTrue(wireBlob.length > 0);
-        
+
         // Verify SSH wire format structure:
         // - 4 bytes: key type length (should be 11 for "ssh-ed25519")
         // - key type string bytes
@@ -101,29 +90,29 @@ class SshKeyUtilsTest {
         // - 32 bytes: public key
         java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap(wireBlob);
         buffer.order(java.nio.ByteOrder.BIG_ENDIAN);
-        
+
         // Read key type length
         int keyTypeLength = buffer.getInt();
         assertEquals(11, keyTypeLength); // "ssh-ed25519" is 11 bytes
-        
+
         // Read key type string
         byte[] keyTypeBytes = new byte[keyTypeLength];
         buffer.get(keyTypeBytes);
         String keyType = new String(keyTypeBytes, 
java.nio.charset.StandardCharsets.UTF_8);
         assertEquals("ssh-ed25519", keyType);
-        
+
         // Read public key length
         int publicKeyLength = buffer.getInt();
         assertEquals(32, publicKeyLength); // Ed25519 public key is 32 bytes
-        
+
         // Read public key
         byte[] publicKeyBytes = new byte[publicKeyLength];
         buffer.get(publicKeyBytes);
         assertEquals(32, publicKeyBytes.length);
-        
+
         // Verify we consumed the entire blob
         assertEquals(0, buffer.remaining());
-        
+
         // Verify comment
         assertEquals("custos-generated", parts[2]);
     }
@@ -133,21 +122,21 @@ class SshKeyUtilsTest {
         // Create test certificate bytes (simplified - just test format)
         byte[] certBytes = new byte[]{1, 2, 3, 4, 5};
         String comment = "test-user";
-        
+
         String certString = 
SshKeyUtils.certBytesToOpenSshCertString(certBytes, comment);
-        
+
         assertNotNull(certString);
         assertTrue(certString.startsWith("[email protected] "));
-        
+
         // Verify format: "[email protected] <base64> <comment>"
         String[] parts = certString.split("\\s+", 3);
         assertEquals(3, parts.length);
         assertEquals("[email protected]", parts[0]);
-        
+
         // Verify base64 decodes back to original bytes
         byte[] decoded = Base64.getDecoder().decode(parts[1]);
         assertArrayEquals(certBytes, decoded);
-        
+
         // Verify comment
         assertEquals(comment, parts[2]);
     }
@@ -155,17 +144,17 @@ class SshKeyUtilsTest {
     @Test
     void testCertBytesToOpenSshCertStringWithoutComment() throws Exception {
         byte[] certBytes = new byte[]{1, 2, 3, 4, 5};
-        
+
         String certString = 
SshKeyUtils.certBytesToOpenSshCertString(certBytes, null);
-        
+
         assertNotNull(certString);
         assertTrue(certString.startsWith("[email protected] "));
-        
+
         // Should have prefix and base64, but no comment
         String[] parts = certString.split("\\s+");
         assertTrue(parts.length >= 2);
         assertEquals("[email protected]", parts[0]);
-        
+
         // Verify base64 decodes back to original bytes
         byte[] decoded = Base64.getDecoder().decode(parts[1]);
         assertArrayEquals(certBytes, decoded);
@@ -199,4 +188,3 @@ class SshKeyUtilsTest {
         });
     }
 }
-
diff --git 
a/signer/signer-service/src/main/java/org/apache/custos/signer/service/ca/SshCertificateSigner.java
 
b/signer/signer-service/src/main/java/org/apache/custos/signer/service/ca/SshCertificateSigner.java
index 3abcc158a..548f027b6 100644
--- 
a/signer/signer-service/src/main/java/org/apache/custos/signer/service/ca/SshCertificateSigner.java
+++ 
b/signer/signer-service/src/main/java/org/apache/custos/signer/service/ca/SshCertificateSigner.java
@@ -35,6 +35,7 @@ import java.security.KeyPair;
 import java.security.MessageDigest;
 import java.security.PrivateKey;
 import java.security.PublicKey;
+import java.security.Signature;
 import java.time.Instant;
 import java.util.Base64;
 import java.util.Collections;
@@ -60,6 +61,7 @@ public class SshCertificateSigner {
     private static final String SSH_KEY_TYPE_ED25519 = "ssh-ed25519";
     private static final String SSH_KEY_TYPE_RSA = "ssh-rsa";
     private static final String SSH_KEY_TYPE_ECDSA = "ecdsa-sha2-nistp256";
+    private static final String SSH_SIG_ALG_RSA_SHA256 = "rsa-sha2-256";
 
     @Autowired
     private OpenBaoClient openBaoClient;
@@ -153,8 +155,17 @@ public class SshCertificateSigner {
             rawKeyBytes = new byte[pkLen];
             buf.get(rawKeyBytes);
         } else {
-            // For future RSA/ECDSA support, fall back to full decoded blob
-            rawKeyBytes = decoded;
+            ByteBuffer buf = ByteBuffer.wrap(decoded);
+            int typeLen = buf.getInt();
+            byte[] typeBytes = new byte[typeLen];
+            buf.get(typeBytes);
+            String embeddedType = new String(typeBytes, 
StandardCharsets.UTF_8);
+            if (!embeddedType.equals(keyType)) {
+                throw new IllegalArgumentException("Mismatched key type inside 
public key blob: " + embeddedType);
+            }
+            byte[] remaining = new byte[buf.remaining()];
+            buf.get(remaining);
+            rawKeyBytes = remaining;
         }
 
         // Calculate fingerprint (SHA256 hash)
@@ -200,8 +211,8 @@ public class SshCertificateSigner {
         cert.setReserved("");
 
         // CA public key
-        // TODO: Support multiple CA key types (RSA, ECDSA)
-        cert.setCaKeyType(SSH_KEY_TYPE_ED25519); // Currently Ed25519 supported
+        String caKeyType = 
normalizeKeyType(caKeyPair.getPublic().getAlgorithm());
+        cert.setCaKeyType(caKeyType);
         try {
             cert.setCaPublicKey(toSshPublicKeyBlob(caKeyPair.getPublic()));
         } catch (Exception e) {
@@ -218,7 +229,6 @@ public class SshCertificateSigner {
         // Serialize certificate for signing
         byte[] certificateData = serializeCertificate(certificate);
 
-        // TODO: Add support for RSA and ECDSA signing algorithms
         // Sign using Ed25519
         // Note: Java reports Ed25519 keys as "EdDSA" algorithm
         String algorithm = caPrivateKey.getAlgorithm();
@@ -229,6 +239,16 @@ public class SshCertificateSigner {
             signer.init(true, privateKeyParams);
             signer.update(certificateData, 0, certificateData.length);
             return signer.generateSignature();
+        } else if ("RSA".equalsIgnoreCase(algorithm)) {
+            Signature signature = Signature.getInstance("SHA256withRSA");
+            signature.initSign(caPrivateKey);
+            signature.update(certificateData);
+            return signature.sign();
+        } else if ("EC".equalsIgnoreCase(algorithm)) {
+            Signature signature = Signature.getInstance("SHA256withECDSA");
+            signature.initSign(caPrivateKey);
+            signature.update(certificateData);
+            return signature.sign();
         } else {
             throw new IllegalArgumentException("Unsupported CA key algorithm: 
" + caPrivateKey.getAlgorithm());
         }
@@ -240,14 +260,12 @@ public class SshCertificateSigner {
     private byte[] createFinalCertificate(SshCertificate certificate, byte[] 
signature) throws Exception {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
 
-        // TODO: Select certificate format based on client key type 
([email protected], [email protected], etc.)
-        // Certificate type
-        writeString(out, "[email protected]");
+        writeString(out, getCertificateType(certificate.getKeyType()));
 
         // Nonce
         writeBytes(out, certificate.getNonce());
 
-        // Subject public key (as SSH wire public key)
+        // Subject public key (SSH wire public key blob)
         writeBytes(out, certificate.getPublicKey());
 
         // Serial
@@ -280,10 +298,9 @@ public class SshCertificateSigner {
         // CA public key (as SSH wire public key)
         writeBytes(out, certificate.getCaPublicKey());
 
-        // TODO: Support signature types for RSA and ECDSA
         // Signature (wrapped as SSH signature blob)
         ByteArrayOutputStream sigBuf = new ByteArrayOutputStream();
-        writeString(sigBuf, "ssh-ed25519");
+        writeString(sigBuf, getSignatureAlgorithm(certificate.getCaKeyType()));
         writeBytes(sigBuf, signature);
         writeBytes(out, sigBuf.toByteArray());
 
@@ -296,14 +313,12 @@ public class SshCertificateSigner {
     private byte[] serializeCertificate(SshCertificate certificate) throws 
Exception {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
 
-        // TODO: Select certificate format based on client key type (currently 
hardcoded to Ed25519)
-        // Certificate type
-        writeString(out, "[email protected]");
+        writeString(out, getCertificateType(certificate.getKeyType()));
 
         // Nonce
         writeBytes(out, certificate.getNonce());
 
-        // Subject public key (as SSH wire public key)
+        // Subject public key (SSH wire public key blob)
         writeBytes(out, certificate.getPublicKey());
 
         // Serial
@@ -366,20 +381,108 @@ public class SshCertificateSigner {
 
     private byte[] toSshPublicKeyBlob(PublicKey publicKey) throws Exception {
         String algorithm = publicKey.getAlgorithm();
+        ByteArrayOutputStream buf = new ByteArrayOutputStream();
         if ("EdDSA".equals(algorithm) || "Ed25519".equals(algorithm) || 
SSH_KEY_TYPE_ED25519.equals(algorithm)) {
             Ed25519PublicKeyParameters publicKeyParams = 
(Ed25519PublicKeyParameters)
                     
org.bouncycastle.crypto.util.PublicKeyFactory.createKey(publicKey.getEncoded());
             byte[] publicKeyBytes = publicKeyParams.getEncoded();
-
-            ByteArrayOutputStream buf = new ByteArrayOutputStream();
             writeString(buf, SSH_KEY_TYPE_ED25519);
             writeBytes(buf, publicKeyBytes);
             return buf.toByteArray();
+
+        } else if ("RSA".equalsIgnoreCase(algorithm)) {
+            java.security.interfaces.RSAPublicKey rsa = 
(java.security.interfaces.RSAPublicKey) publicKey;
+            writeString(buf, SSH_KEY_TYPE_RSA);
+            writeMpInt(buf, rsa.getPublicExponent());
+            writeMpInt(buf, rsa.getModulus());
+            return buf.toByteArray();
+
+        } else if ("EC".equalsIgnoreCase(algorithm)) {
+            java.security.interfaces.ECPublicKey ec = 
(java.security.interfaces.ECPublicKey) publicKey;
+            String curve = "nistp256";
+            byte[] q = encodeEcPoint(ec.getW(), ec.getParams());
+            writeString(buf, SSH_KEY_TYPE_ECDSA);
+            writeString(buf, curve);
+            writeBytes(buf, q);
+            return buf.toByteArray();
         }
 
         throw new IllegalArgumentException("Unsupported CA public key type: " 
+ algorithm);
     }
 
+    private String getCertificateType(String keyType) {
+        if (keyType == null) {
+            return SSH_KEY_TYPE_ED25519 + "[email protected]";
+        }
+        if (keyType.startsWith(SSH_KEY_TYPE_RSA)) {
+            return SSH_KEY_TYPE_RSA + "[email protected]";
+        }
+        if (keyType.startsWith(SSH_KEY_TYPE_ECDSA)) {
+            return SSH_KEY_TYPE_ECDSA + "[email protected]";
+        }
+        return SSH_KEY_TYPE_ED25519 + "[email protected]";
+    }
+
+    private String getSignatureAlgorithm(String caKeyType) {
+        if (caKeyType == null) {
+            return SSH_KEY_TYPE_ED25519;
+        }
+        if (caKeyType.startsWith(SSH_KEY_TYPE_RSA)) {
+            return SSH_SIG_ALG_RSA_SHA256;
+        }
+        if (caKeyType.startsWith(SSH_KEY_TYPE_ECDSA)) {
+            return SSH_KEY_TYPE_ECDSA;
+        }
+        return SSH_KEY_TYPE_ED25519;
+    }
+
+    private String normalizeKeyType(String algorithm) {
+        if (algorithm == null) {
+            return SSH_KEY_TYPE_ED25519;
+        }
+        if ("RSA".equalsIgnoreCase(algorithm)) {
+            return SSH_KEY_TYPE_RSA;
+        }
+        if ("EC".equalsIgnoreCase(algorithm)) {
+            return SSH_KEY_TYPE_ECDSA;
+        }
+        if ("Ed25519".equalsIgnoreCase(algorithm) || 
"EdDSA".equalsIgnoreCase(algorithm)) {
+            return SSH_KEY_TYPE_ED25519;
+        }
+        return algorithm;
+    }
+
+    private static byte[] encodeEcPoint(java.security.spec.ECPoint point, 
java.security.spec.ECParameterSpec params) {
+        int fieldSize = (params.getCurve().getField().getFieldSize() + 7) / 8;
+        byte[] x = toFixedLength(point.getAffineX(), fieldSize);
+        byte[] y = toFixedLength(point.getAffineY(), fieldSize);
+        byte[] q = new byte[1 + x.length + y.length];
+        q[0] = 0x04;
+        System.arraycopy(x, 0, q, 1, x.length);
+        System.arraycopy(y, 0, q, 1 + x.length, y.length);
+        return q;
+    }
+
+    private static byte[] toFixedLength(java.math.BigInteger value, int 
length) {
+        byte[] bytes = value.toByteArray();
+        if (bytes.length == length) {
+            return bytes;
+        }
+        byte[] out = new byte[length];
+        if (bytes.length > length) {
+            System.arraycopy(bytes, bytes.length - length, out, 0, length);
+        } else {
+            System.arraycopy(bytes, 0, out, length - bytes.length, 
bytes.length);
+        }
+        return out;
+    }
+
+    private void writeMpInt(ByteArrayOutputStream out, java.math.BigInteger 
value) throws Exception {
+        byte[] bytes = value.toByteArray();
+        writeUint32(out, bytes.length);
+        out.write(bytes);
+    }
+
     private void writeString(ByteArrayOutputStream out, String str) throws 
Exception {
         byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
         writeUint32(out, bytes.length);
diff --git 
a/signer/signer-service/src/main/java/org/apache/custos/signer/service/policy/KeyType.java
 
b/signer/signer-service/src/main/java/org/apache/custos/signer/service/policy/KeyType.java
new file mode 100644
index 000000000..c30cb781c
--- /dev/null
+++ 
b/signer/signer-service/src/main/java/org/apache/custos/signer/service/policy/KeyType.java
@@ -0,0 +1,48 @@
+/*
+ * 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 specific language
+ * governing permissions and limitations under the License.
+ *
+ */
+package org.apache.custos.signer.service.policy;
+
+// TODO: move to shared/common module for reuse with SDK
+public enum KeyType {
+    ED25519("ed25519"),
+    RSA("rsa"),
+    ECDSA("ecdsa");
+
+    private final String id;
+
+    KeyType(String id) {
+        this.id = id;
+    }
+
+    public String id() {
+        return id;
+    }
+
+    public static KeyType from(String value) {
+        if (value == null) {
+            return ED25519;
+        }
+        String normalized = value.toLowerCase();
+        return switch (normalized) {
+            case "rsa", "ssh-rsa" -> RSA;
+            case "ecdsa", "ecdsa-sha2-nistp256" -> ECDSA;
+            default -> ED25519;
+        };
+    }
+}
diff --git 
a/signer/signer-service/src/main/java/org/apache/custos/signer/service/policy/PolicyEnforcer.java
 
b/signer/signer-service/src/main/java/org/apache/custos/signer/service/policy/PolicyEnforcer.java
index 4424cefa6..e6d184916 100644
--- 
a/signer/signer-service/src/main/java/org/apache/custos/signer/service/policy/PolicyEnforcer.java
+++ 
b/signer/signer-service/src/main/java/org/apache/custos/signer/service/policy/PolicyEnforcer.java
@@ -179,12 +179,7 @@ public class PolicyEnforcer {
             String[] parts = publicKeyString.split("\\s+");
 
             if (parts.length >= 1) {
-                String keyType = parts[0];
-                // Normalize key type: remove "ssh-" prefix if present (e.g., 
"ssh-ed25519" -> "ed25519")
-                if (keyType.startsWith("ssh-")) {
-                    keyType = keyType.substring(4);
-                }
-                return keyType;
+                return KeyType.from(parts[0]).id();
             }
 
             throw new IllegalArgumentException("Invalid SSH public key 
format");


Reply via email to