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

commit aa09183993bbde86be13c2c1243051ef367746d0
Author: lahiruj <[email protected]>
AuthorDate: Mon Nov 24 23:59:46 2025 -0500

    add requestCertificateMaterials method for direct certificate material 
access
---
 signer/README.md                                   |  31 ++++
 signer/signer-sdk-core/pom.xml                     |   7 -
 .../custos/signer/sdk/CertificateMaterials.java    |  61 +++++++
 .../org/apache/custos/signer/sdk/SshClient.java    | 123 +++++++++----
 .../apache/custos/signer/sdk/util/SshKeyUtils.java | 166 +++++++++++++++++
 .../custos/signer/sdk/util/SshKeyUtilsTest.java    | 202 +++++++++++++++++++++
 .../signer/service/ca/SshCertificateSigner.java    |   7 +-
 .../custos/signer/service/vault/OpenBaoClient.java |   5 +-
 8 files changed, 552 insertions(+), 50 deletions(-)

diff --git a/signer/README.md b/signer/README.md
index 96c47ac28..c6ed0740c 100644
--- a/signer/README.md
+++ b/signer/README.md
@@ -210,6 +210,37 @@ download("/remote/output.txt","/local/output.txt");
 }
 ```
 
+### Request Certificate Materials
+
+Get all SSH connection materials (private key, public key, certificate, and 
metadata) to use with your preferred SSH client or library:
+
+```java
+// Request certificate materials
+CertificateMaterials materials = sshClient.requestCertificateMaterials(
+    "hpcA", "user", 3600, oidcToken
+);
+
+// Use materials with OpenSSH command-line or other SSH libraries
+java.nio.file.Path keyFile = java.nio.file.Files.createTempFile("ssh-key", "");
+java.nio.file.Path certFile = java.nio.file.Files.createTempFile("ssh-cert", 
"");
+
+java.nio.file.Files.write(keyFile, materials.getPrivateKeyPem().getBytes());
+java.nio.file.Files.write(certFile, materials.getOpensshCert().getBytes());
+
+// Use with OpenSSH
+// ssh -i keyFile -o CertificateFile=certFile user@host
+
+// Or use KeyPair object directly with other SSH libraries
+KeyPair keyPair = materials.getKeyPair();
+byte[] certBytes = materials.getCertBytes();
+```
+
+**OpenSSH Certificate Format**: The `opensshCert` field contains the 
certificate in OpenSSH string format:
+```
[email protected] <base64-encoded-certificate> <comment>
+```
+This format can be written directly to a file and used with OpenSSH 
command-line tools using the `-o CertificateFile=` option.
+
 ### Configuration File
 
 Create `custos-sdk.yml`:
diff --git a/signer/signer-sdk-core/pom.xml b/signer/signer-sdk-core/pom.xml
index 1124e181d..4bfbad373 100644
--- a/signer/signer-sdk-core/pom.xml
+++ b/signer/signer-sdk-core/pom.xml
@@ -106,13 +106,6 @@
                     <target>17</target>
                 </configuration>
             </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <configuration>
-                    <groups>unit</groups>
-                </configuration>
-            </plugin>
         </plugins>
     </build>
 
diff --git 
a/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/CertificateMaterials.java
 
b/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/CertificateMaterials.java
new file mode 100644
index 000000000..596c7dcfc
--- /dev/null
+++ 
b/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/CertificateMaterials.java
@@ -0,0 +1,61 @@
+/*
+ * 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 java.security.KeyPair;
+import java.time.Instant;
+
+/**
+ * Immutable response containing all SSH connection materials.
+ * <p>
+ * This class provides everything needed to establish an SSH connection:
+ * - Private and public keys (both as objects and string formats)
+ * - Signed certificate (both as bytes and OpenSSH string format)
+ * - Connection metadata (target host, port, username)
+ * - Certificate metadata (serial, validity, CA fingerprint)
+ * <p>
+ *
+ * @param keyPair          In-memory keypair object
+ * @param privateKeyPem    Private key in PEM format
+ * @param publicKeyOpenSsh Public key in OpenSSH format
+ * @param opensshCert      Certificate in OpenSSH string format
+ * @param certBytes        Certificate bytes (defensively copied in and out)
+ * @param serial           Certificate serial number
+ * @param validAfter       Certificate validity start
+ * @param validBefore      Certificate validity end
+ * @param caFingerprint    CA fingerprint
+ * @param targetHost       Target SSH host
+ * @param targetPort       Target SSH port
+ * @param targetUsername   Target SSH username
+ */
+public record CertificateMaterials(KeyPair keyPair, String privateKeyPem, 
String publicKeyOpenSsh, String opensshCert,
+                                   byte[] certBytes, long serial, Instant 
validAfter, Instant validBefore,
+                                   String caFingerprint, String targetHost, 
int targetPort, String targetUsername) {
+
+    public CertificateMaterials {
+        // Defensive copy of certBytes
+        certBytes = (certBytes != null) ? certBytes.clone() : null;
+    }
+
+    @Override
+    public byte[] certBytes() {
+        return certBytes != null ? certBytes.clone() : null;
+    }
+}
+
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 2ad3fb4ca..6b1881218 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
@@ -23,13 +23,13 @@ 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.sdk.ssh.SshSession;
+import org.apache.custos.signer.sdk.util.SshKeyUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.nio.charset.StandardCharsets;
 import java.security.KeyPair;
-import java.security.KeyPairGenerator;
 import java.time.Duration;
-import java.util.Base64;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -60,32 +60,31 @@ public class SshClient implements AutoCloseable {
     }
 
     /**
-     * Open an SSH session using client alias
+     * Request certificate materials (keys and certificate).
+     * This method returns all SSH connection materials (private key, public 
key, certificate, and metadata).
      *
      * @param clientAlias Alias configured in SDK configuration
      * @param principal   SSH username (typically from OIDC token)
      * @param ttlSeconds  Certificate TTL in seconds
      * @param userToken   OIDC user access token
-     * @return SSH session for remote operations
+     * @return CertificateMaterials containing all SSH connection materials
+     * @throws SshClientException if certificate request fails
      */
-    public SshSession openSession(String clientAlias, String principal, int 
ttlSeconds, String userToken) {
+    public CertificateMaterials requestCertificateMaterials(String 
clientAlias, String principal, int ttlSeconds, String userToken) {
         try {
-            logger.debug("Opening SSH session for client: {}, principal: {}, 
ttl: {}s",
-                    clientAlias, principal, ttlSeconds);
+            logger.debug("Requesting certificate materials for client: {}, 
principal: {}, ttl: {}s", clientAlias, principal, ttlSeconds);
 
             // Resolve client alias to configuration
             SdkConfiguration.ClientConfig clientConfig = 
configuration.getClientConfig(clientAlias)
                     .orElseThrow(() -> new IllegalArgumentException("Client 
alias not found: " + clientAlias));
 
-            // Generate ephemeral SSH keypair
-            KeyPair keyPair = generateKeyPair();
-            String contextId = generateContextId(clientAlias, principal);
+            // TODO: Support RSA and ECDSA key types in addition to Ed25519
+            // Generate ephemeral Ed25519 keypair
+            KeyPair keyPair = SshKeyUtils.generateEd25519KeyPair();
 
-            // Store keypair temporarily
-            keyStore.store(principal, contextId, keyPair, 
Duration.ofSeconds(ttlSeconds));
-
-            // Convert public key to SSH format
-            byte[] publicKeyBytes = toSshPublicKeyFormat(keyPair);
+            // Convert public key to OpenSSH format
+            String publicKeyOpenSsh = 
SshKeyUtils.keyPairToOpenSshPublicKey(keyPair);
+            byte[] publicKeyBytes = 
publicKeyOpenSsh.getBytes(StandardCharsets.UTF_8);
 
             // Request certificate from signer service
             SshSignerClient.CertificateResponse certResponse = 
signerClient.requestCertificate(
@@ -97,26 +96,84 @@ public class SshClient implements AutoCloseable {
                     userToken
             );
 
-            logger.debug("Received certificate: serial={}, target={}:{}",
-                    certResponse.getSerialNumber(), 
certResponse.getTargetHost(), certResponse.getTargetPort());
+            logger.debug("Received certificate: serial={}, target={}:{}", 
certResponse.getSerialNumber(), certResponse.getTargetHost(), 
certResponse.getTargetPort());
 
-            // Create SSH session
-            SshSession session = new SshSession(
-                    certResponse.getTargetHost(),
-                    certResponse.getTargetPort(),
-                    certResponse.getTargetUsername(),
+            // Convert private key to PEM format
+            String privateKeyPem = SshKeyUtils.keyPairToPem(keyPair);
+
+            // Convert certificate bytes to OpenSSH cert string format
+            // Use principal as comment
+            String opensshCert = SshKeyUtils.certBytesToOpenSshCertString(
+                    certResponse.getCertificate(), principal);
+
+            // Create defensive copy of certBytes
+            byte[] certBytesCopy = certResponse.getCertificate().clone();
+
+            // Build and return CertificateMaterials
+            CertificateMaterials materials = new CertificateMaterials(
                     keyPair,
-                    certResponse.getCertificate(),
+                    privateKeyPem,
+                    publicKeyOpenSsh,
+                    opensshCert,
+                    certBytesCopy,
+                    certResponse.getSerialNumber(),
                     certResponse.getValidAfter(),
-                    certResponse.getValidBefore()
+                    certResponse.getValidBefore(),
+                    certResponse.getCaFingerprint(),
+                    certResponse.getTargetHost(),
+                    certResponse.getTargetPort(),
+                    certResponse.getTargetUsername()
+            );
+
+            logger.info("Certificate materials prepared: client={}, 
principal={}, serial={}", clientAlias, principal, 
certResponse.getSerialNumber());
+
+            return materials;
+
+        } catch (Exception e) {
+            logger.error("Failed to request certificate materials for client: 
{}, principal: {}", clientAlias, principal, e);
+            throw new SshClientException("Failed to request certificate 
materials", e);
+        }
+    }
+
+    /**
+     * Open an SSH session using client alias.
+     * <p>
+     * This method uses {@link #requestCertificateMaterials(String, String, 
int, String)}
+     * internally and then manages the session lifecycle with keystore 
tracking.
+     *
+     * @param clientAlias Alias configured in SDK configuration
+     * @param principal   SSH username (typically from OIDC token)
+     * @param ttlSeconds  Certificate TTL in seconds
+     * @param userToken   OIDC user access token
+     * @return SSH session for remote operations
+     */
+    public SshSession openSession(String clientAlias, String principal, int 
ttlSeconds, String userToken) {
+        try {
+            logger.debug("Opening SSH session for client: {}, principal: {}, 
ttl: {}s", clientAlias, principal, ttlSeconds);
+
+            // Request certificate materials (shared flow)
+            CertificateMaterials materials = 
requestCertificateMaterials(clientAlias, principal, ttlSeconds, userToken);
+
+            // Store keypair in keystore for session tracking
+            String contextId = generateContextId(clientAlias, principal);
+            keyStore.store(principal, contextId, materials.keyPair(), 
Duration.ofSeconds(ttlSeconds));
+
+            // Create SSH session from materials
+            SshSession session = new SshSession(
+                    materials.targetHost(),
+                    materials.targetPort(),
+                    materials.targetUsername(),
+                    materials.keyPair(),
+                    materials.certBytes(),
+                    materials.validAfter(),
+                    materials.validBefore()
             );
 
             // Track active session for cleanup
             String sessionId = generateSessionId(clientAlias, principal);
             activeSessions.put(sessionId, session);
 
-            logger.info("SSH session opened: client={}, principal={}, 
target={}:{}",
-                    clientAlias, principal, certResponse.getTargetHost(), 
certResponse.getTargetPort());
+            logger.info("SSH session opened: client={}, principal={}, 
target={}:{}", clientAlias, principal, materials.targetHost(), 
materials.targetPort());
 
             return session;
 
@@ -196,11 +253,6 @@ public class SshClient implements AutoCloseable {
         logger.debug("SshClient closed successfully");
     }
 
-    private KeyPair generateKeyPair() throws Exception {
-        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
-        return keyGen.generateKeyPair();
-    }
-
     private String generateContextId(String clientAlias, String principal) {
         return clientAlias + "-" + principal + "-" + 
System.currentTimeMillis();
     }
@@ -209,15 +261,6 @@ public class SshClient implements AutoCloseable {
         return clientAlias + "-" + principal + "-" + 
System.currentTimeMillis();
     }
 
-    private byte[] toSshPublicKeyFormat(KeyPair keyPair) throws Exception {
-        // Convert KeyPair to SSH public key format
-        // TODO - use a proper SSH key library
-        String keyType = "ssh-ed25519";
-        String publicKeyBase64 = 
Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
-        String sshKey = keyType + " " + publicKeyBase64 + " custos-generated";
-        return sshKey.getBytes();
-    }
-
     /**
      * Builder for SshClient
      */
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
new file mode 100644
index 000000000..132a6638f
--- /dev/null
+++ 
b/signer/signer-sdk-core/src/main/java/org/apache/custos/signer/sdk/util/SshKeyUtils.java
@@ -0,0 +1,166 @@
+/*
+ * 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.util;
+
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+
+import java.io.StringWriter;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+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();
+    }
+
+    /**
+     * Convert a private key to PEM format using Bouncy Castle.
+     *
+     * @param keyPair KeyPair containing the private key
+     * @return PEM-formatted private key string
+     * @throws Exception if conversion fails
+     */
+    public static String keyPairToPem(KeyPair keyPair) throws Exception {
+        if (keyPair == null || keyPair.getPrivate() == null) {
+            throw new IllegalArgumentException("KeyPair or private key is 
null");
+        }
+
+        StringWriter writer = new StringWriter();
+        try (JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) {
+            pemWriter.writeObject(keyPair.getPrivate());
+        }
+        return writer.toString();
+    }
+
+    /**
+     * 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);
+
+            blob.writeInt(publicKeyBytes.length);
+
+            blob.write(publicKeyBytes);
+
+            blob.flush();
+        } finally {
+            blob.close();
+        }
+
+        // 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";
+    }
+
+    /**
+     * Convert certificate bytes to OpenSSH certificate string format.
+     * <p>
+     * Format: "[email protected] 
&lt;base64-encoded-certificate&gt; &lt;comment&gt;"
+     * <p>
+     * The certificate bytes are base64-encoded and prefixed with the 
appropriate
+     * 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 {
+        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);
+
+        // Format: "[email protected] <base64> <comment>"
+        String certString = SSH_CERT_TYPE_ED25519 + " " + base64Cert;
+        if (comment != null && !comment.isEmpty()) {
+            certString += " " + comment;
+        }
+
+        return certString;
+    }
+}
+
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
new file mode 100644
index 000000000..1d2cd65bd
--- /dev/null
+++ 
b/signer/signer-sdk-core/src/test/java/org/apache/custos/signer/sdk/util/SshKeyUtilsTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.util;
+
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+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.*;
+
+/**
+ * Unit tests for SshKeyUtils
+ */
+class SshKeyUtilsTest {
+
+    @Test
+    void testGenerateEd25519KeyPair() throws Exception {
+        KeyPair keyPair = SshKeyUtils.generateEd25519KeyPair();
+        
+        assertNotNull(keyPair);
+        assertNotNull(keyPair.getPrivate());
+        assertNotNull(keyPair.getPublic());
+        // Java reports Ed25519 as "EdDSA"
+        assertEquals("EdDSA", keyPair.getPublic().getAlgorithm());
+    }
+
+    @Test
+    void testKeyPairToPem() throws Exception {
+        KeyPair keyPair = SshKeyUtils.generateEd25519KeyPair();
+        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());
+        }
+    }
+
+    @Test
+    void testKeyPairToOpenSshPublicKey() throws Exception {
+        KeyPair keyPair = SshKeyUtils.generateEd25519KeyPair();
+        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
+        // - 4 bytes: public key length (should be 32)
+        // - 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]);
+    }
+
+    @Test
+    void testCertBytesToOpenSshCertString() throws Exception {
+        // 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]);
+    }
+
+    @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);
+    }
+
+    @Test
+    void testKeyPairToPemWithNullKeyPair() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            SshKeyUtils.keyPairToPem(null);
+        });
+    }
+
+    @Test
+    void testKeyPairToOpenSshPublicKeyWithNullKeyPair() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            SshKeyUtils.keyPairToOpenSshPublicKey(null);
+        });
+    }
+
+    @Test
+    void testCertBytesToOpenSshCertStringWithNullBytes() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            SshKeyUtils.certBytesToOpenSshCertString(null, "comment");
+        });
+    }
+
+    @Test
+    void testCertBytesToOpenSshCertStringWithEmptyBytes() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            SshKeyUtils.certBytesToOpenSshCertString(new byte[0], "comment");
+        });
+    }
+}
+
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 988a39f28..80e1dda85 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
@@ -175,7 +175,8 @@ public class SshCertificateSigner {
         cert.setReserved("");
 
         // CA public key
-        cert.setCaKeyType(SSH_KEY_TYPE_ED25519); // Assuming CA is Ed25519
+        // TODO: Support multiple CA key types (RSA, ECDSA)
+        cert.setCaKeyType(SSH_KEY_TYPE_ED25519); // Currently Ed25519 supported
         cert.setCaPublicKey(extractPublicKeyBytes(caKeyPair.getPublic()));
 
         return cert;
@@ -188,6 +189,7 @@ public class SshCertificateSigner {
         // Serialize certificate for signing
         byte[] certificateData = serializeCertificate(certificate);
 
+        // TODO: Add support for RSA and ECDSA signing algorithms
         // Sign using Ed25519
         if (caPrivateKey.getAlgorithm().equals("Ed25519")) {
             Ed25519Signer signer = new Ed25519Signer();
@@ -207,6 +209,7 @@ 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.)
         // Write certificate type
         writeString(out, "[email protected]");
 
@@ -246,6 +249,7 @@ public class SshCertificateSigner {
         writeString(out, certificate.getCaKeyType());
         writeBytes(out, certificate.getCaPublicKey());
 
+        // TODO: Support signature types for RSA and ECDSA
         // Write signature
         writeString(out, "ssh-ed25519");
         writeBytes(out, signature);
@@ -259,6 +263,7 @@ 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)
         // Write certificate type
         writeString(out, "[email protected]");
 
diff --git 
a/signer/signer-service/src/main/java/org/apache/custos/signer/service/vault/OpenBaoClient.java
 
b/signer/signer-service/src/main/java/org/apache/custos/signer/service/vault/OpenBaoClient.java
index 67ccee82b..7548d0b57 100644
--- 
a/signer/signer-service/src/main/java/org/apache/custos/signer/service/vault/OpenBaoClient.java
+++ 
b/signer/signer-service/src/main/java/org/apache/custos/signer/service/vault/OpenBaoClient.java
@@ -171,8 +171,9 @@ public class OpenBaoClient {
                 vaultOperations.write(currentPath, currentData);
             }
 
+            // TODO: Support RSA and ECDSA CA key types
             // Generate new next key
-            KeyPair newNextKey = generateKeyPair("ed25519"); // Default to 
ed25519
+            KeyPair newNextKey = generateKeyPair("ed25519");
             String nextPrivateKeyPem = toPemString(newNextKey.getPrivate());
             String nextPublicKeyPem = toPemString(newNextKey.getPublic());
 
@@ -292,7 +293,7 @@ public class OpenBaoClient {
     }
 
     private KeyPair generateKeyPair(String algorithm) throws Exception {
-        // For now, only support ed25519. Can be extended to support RSA, ECDSA
+        // TODO: Add support for RSA and ECDSA key generation
         if ("ed25519".equalsIgnoreCase(algorithm)) {
             return generateEd25519KeyPair();
         } else {

Reply via email to