Repository: udondan/cdk-ec2-key-pair (language: python)

Issue:
Add support type of Key: RSA or ed25519
In a current moment Key pairs  support 2 types: RSA and ed25519. 

If it possible I want to ask to add opportunity to choose key type


Discussion / hints:
Hi Dmitry,

I looked into this for quite some time but wasn't able to figure it out. I created #39, which so far is a partial implementation.

The problem is, the [forge](https://github.com/digitalbazaar/forge) library seems to have limited support for ed25519 keys. I haven't found a way to generate the public key from the key pair.

Maybe in a future version of forge.

Cheers,
Daniel
@udondan we need this feature for Amazon Linux 2023 https://docs.aws.amazon.com/linux/al2023/ug/ssh-host-keys-disabled.html

Fix patch:
diff --git a/README.md b/README.md
index 4bd82ff0..dbf13595 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,18 @@
 
 [AWS CDK] L3 construct for managing [EC2 Key Pairs].
 
+Manages RSA and ED25519 Key Pairs in EC2 through a Lambda function.
+
+Support for public key format in:
+
+- OpenSSH
+- ssh
+- PEM
+- PKCS#1
+- PKCS#8
+- RFC4253 (Base64 encoded)
+- PuTTY ppk
+
 > [!NOTE]
 > Please be aware, CloudFormation now natively supports creating EC2 Key Pairs via [AWS::EC2::KeyPair](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-keypair.html), so you can generally use [CDK's own KeyPair construct](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.KeyPair.html). There are a few differences, though, and this is why the custom construct remains valuable:
 >
@@ -42,7 +54,7 @@ import { KeyPair } from 'cdk-ec2-key-pair';
 
 // Create the Key Pair
 const key = new KeyPair(this, 'A-Key-Pair', {
-  name: 'a-key-pair',
+  keyPairName: 'a-key-pair',
   description: 'This is a Key Pair',
   storePublicKey: true, // by default the public key will not be stored in Secrets Manager
 });
@@ -55,7 +67,7 @@ key.grantReadOnPublicKey(anotherRole);
 
 // Use Key Pair on an EC2 instance
 new ec2.Instance(this, 'An-Instance', {
-  keyName: key.keyPairName,
+  keyPair: key,
   // ...
 });
 ```
@@ -97,7 +109,7 @@ To use a custom KMS key you can pass it to the Key Pair:
 const kmsKey = new kms.Key(this, 'KMS-key');
 
 const keyPair = new KeyPair(this, 'A-Key-Pair', {
-  name: 'a-key-pair',
+  keyPairName: 'a-key-pair',
   kms: kmsKey,
 });
 ```
@@ -111,7 +123,7 @@ const kmsKeyPrivate = new kms.Key(this, 'KMS-key-private');
 const kmsKeyPublic = new kms.Key(this, 'KMS-key-public');
 
 const keyPair = new KeyPair(this, 'A-Key-Pair', {
-  name: 'a-key-pair',
+  keyPairName: 'a-key-pair',
   kmsPrivateKey: kmsKeyPrivate,
   kmsPublicKey: kmsKeyPublic,
 });
@@ -125,7 +137,7 @@ The public key has to be in OpenSSH format.
 
 ```typescript
 new KeyPair(this, 'Test-Key-Pair', {
-  name: 'imported-key-pair',
+  keyPairName: 'imported-key-pair',
   publicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCuMmbK...',
 });
 ```
@@ -139,7 +151,7 @@ You also have to set `exposePublicKey` to `true` so you can actually get the pub
 
 ```typescript
 const key = new KeyPair(this, 'Signing-Key-Pair', {
-  name: 'CFN-signing-key',
+  keyPairName: 'CFN-signing-key',
   exposePublicKey: true,
   storePublicKey: true,
   publicKeyFormat: PublicKeyFormat.PEM,
diff --git a/lambda/index.ts b/lambda/index.ts
index 804e83c4..6e9f033a 100644
--- a/lambda/index.ts
+++ b/lambda/index.ts
@@ -45,7 +45,7 @@ import {
   Logger,
   StandardLogger,
 } from 'aws-cloudformation-custom-resource';
-import * as forge from 'node-forge';
+import { parsePrivateKey } from 'sshpk';
 import { PublicKeyFormat, ResourceProperties } from './types';
 
 const ec2Client = new EC2Client({});
@@ -63,6 +63,7 @@ export const handler = function (
     updateResource,
     deleteResource,
   );
+
   if (event.ResourceProperties.LogLevel) {
     resource.setLogger(
       new StandardLogger(
@@ -111,6 +112,13 @@ async function updateResource(
     );
   }
 
+  const oldKeyType = resource.event.OldResourceProperties?.KeyType ?? 'rsa'; // we added this feature later, so there might be keys w/o a stored type
+  if (resource.event.ResourceProperties.KeyType !== oldKeyType) {
+    throw new Error(
+      'The type of a key pair cannot be changed. Please create a new key pair instead',
+    );
+  }
+
   const keyPair = await updateKeyPair(resource, log);
   await updateKeyPairAddTags(resource, log, keyPair.KeyPairId!);
   await updateKeyPairRemoveTags(resource, log, keyPair.KeyPairId!);
@@ -182,6 +190,7 @@ async function createKeyPair(
     const params: CreateKeyPairCommandInput = {
       /* eslint-disable @typescript-eslint/naming-convention */
       KeyName: resource.properties.Name.value,
+      KeyType: resource.properties.KeyType.value,
       TagSpecifications: [
         {
           ResourceType: 'key-pair',
@@ -310,6 +319,11 @@ async function deleteKeyPair(
   log: Logger,
 ): Promise<void> {
   log.debug('called function deleteKeyPair');
+  const keyPairName = resource.properties.Name.value;
+  if (!(await keyPairExists(keyPairName, log))) {
+    log.warn(`Key Pair "${keyPairName}" does not exist. Nothing to delete`);
+    return;
+  }
   const params: DeleteKeyPairCommandInput = {
     /* eslint-disable-next-line @typescript-eslint/naming-convention */
     KeyName: resource.properties.Name.value,
@@ -476,20 +490,11 @@ async function makePublicKey(
     (keyPair as CreateKeyPairCommandOutput).KeyMaterial ??
     (await getPrivateKey(resource, log));
 
-  const privateKey = forge.pki.privateKeyFromPem(keyMaterial);
-  const forgePublicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
-
-  const publicKeyFormat = resource.properties.PublicKeyFormat.value;
-  switch (publicKeyFormat) {
-    case PublicKeyFormat.PEM:
-      return forge.pki.publicKeyToPem(forgePublicKey);
-    case PublicKeyFormat.OPENSSH:
-      return forge.ssh.publicKeyToOpenSSH(forgePublicKey);
-    default:
-      throw new Error(
-        `Unsupported public key format ${publicKeyFormat as string}`,
-      );
-  }
+  const privateKey = parsePrivateKey(keyMaterial);
+  privateKey.comment = resource.properties.Name.value;
+  return privateKey
+    .toPublic()
+    .toString(resource.properties.PublicKeyFormat.value);
 }
 
 async function exposePublicKey(
@@ -505,6 +510,10 @@ async function exposePublicKey(
     } else {
       publicKey = await makePublicKey(resource, log, keyPair);
     }
+    if (resource.properties.PublicKeyFormat.value === PublicKeyFormat.RFC4253) {
+      // CloudFormation cannot deal with binary data, so we need to encode the public key
+      publicKey = Buffer.from(publicKey).toString('base64');
+    }
     resource.addResponseValue('PublicKeyValue', publicKey);
   } else {
     resource.addResponseValue(
@@ -565,6 +574,7 @@ async function deletePrivateKeySecret(
   log.debug('called function deletePrivateKeySecret');
   const arn = `${resource.properties.SecretPrefix.value}${resource.properties.Name.value}/private`;
   if (!(await secretExists(arn, log))) {
+    log.warn(`Secret "${arn}" does not exist. Nothing to delete`);
     return;
   }
   const result = await deleteSecret(resource, log, arn);
@@ -578,6 +588,7 @@ async function deletePublicKeySecret(
   log.debug('called function deletePublicKeySecret');
   const arn = `${resource.properties.SecretPrefix.value}${resource.properties.Name.value}/public`;
   if (!(await secretExists(arn, log))) {
+    log.warn(`Secret "${arn}" does not exist. Nothing to delete`);
     return;
   }
   const result = await deleteSecret(resource, log, arn);
@@ -611,6 +622,28 @@ async function secretExists(name: string, log: Logger): Promise<boolean> {
   }
 }
 
+async function keyPairExists(name: string, log: Logger): Promise<boolean> {
+  log.debug('called function keyPairExists');
+  const params: DescribeKeyPairsCommandInput = {
+    /* eslint-disable-next-line @typescript-eslint/naming-convention */
+    KeyNames: [name],
+  };
+  log.debug('ec2.describeKeyPairs:', JSON.stringify(params, null, 2));
+  try {
+    const result = await ec2Client.send(new DescribeKeyPairsCommand(params));
+    return (result.KeyPairs?.length ?? 0) > 0;
+  } catch (error) {
+    if (error.name && error.name == 'InvalidKeyPair.NotFound') {
+      return false;
+    }
+    if (error instanceof ResourceNotFoundException) {
+      return false;
+    } else {
+      throw error;
+    }
+  }
+}
+
 function deleteSecret(
   resource: CustomResource<ResourceProperties>,
   log: Logger,
diff --git a/lambda/package-lock.json b/lambda/package-lock.json
index 9c1a2c0f..abdb30d0 100644
--- a/lambda/package-lock.json
+++ b/lambda/package-lock.json
@@ -6,7 +6,23 @@
     "": {
       "dependencies": {
         "aws-cloudformation-custom-resource": "5.0.0",
-        "node-forge": "1.3.1"
+        "sshpk": "1.18.0"
+      }
+    },
+    "node_modules/asn1": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
+      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+      "dependencies": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "node_modules/assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
+      "engines": {
+        "node": ">=0.8"
       }
     },
     "node_modules/aws-cloudformation-custom-resource": {
@@ -14,25 +30,164 @@
       "resolved": "https://registry.npmjs.org/aws-cloudformation-custom-resource/-/aws-cloudformation-custom-resource-5.0.0.tgz",
       "integrity": "sha512-ekTL9j/Pb5T7S6RVnlWDiDoBH0uT4OBMJ2d1AMATX2ULvXz6hhr3OL2aRLZB1kf8ekrNghJtOYcmiMmlZ7s8Ow=="
     },
-    "node_modules/node-forge": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
-      "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+    "node_modules/bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
+      "dependencies": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "node_modules/dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
+      "dependencies": {
+        "assert-plus": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
+      "dependencies": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "node_modules/getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
+      "dependencies": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "node_modules/jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "node_modules/sshpk": {
+      "version": "1.18.0",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
+      "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
+      "dependencies": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      },
+      "bin": {
+        "sshpk-conv": "bin/sshpk-conv",
+        "sshpk-sign": "bin/sshpk-sign",
+        "sshpk-verify": "bin/sshpk-verify"
+      },
       "engines": {
-        "node": ">= 6.13.0"
+        "node": ">=0.10.0"
       }
+    },
+    "node_modules/tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
     }
   },
   "dependencies": {
+    "asn1": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
+      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
+    },
     "aws-cloudformation-custom-resource": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/aws-cloudformation-custom-resource/-/aws-cloudformation-custom-resource-5.0.0.tgz",
       "integrity": "sha512-ekTL9j/Pb5T7S6RVnlWDiDoBH0uT4OBMJ2d1AMATX2ULvXz6hhr3OL2aRLZB1kf8ekrNghJtOYcmiMmlZ7s8Ow=="
     },
-    "node-forge": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
-      "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "sshpk": {
+      "version": "1.18.0",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
+      "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
+      "requires": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
     }
   }
 }
diff --git a/lambda/package.json b/lambda/package.json
index 7ff8470e..82a3f834 100644
--- a/lambda/package.json
+++ b/lambda/package.json
@@ -1,6 +1,6 @@
 {
   "dependencies": {
     "aws-cloudformation-custom-resource": "5.0.0",
-    "node-forge": "1.3.1"
+    "sshpk": "1.18.0"
   }
 }
diff --git a/lambda/types.ts b/lambda/types.ts
index dab98e57..2a2c043b 100644
--- a/lambda/types.ts
+++ b/lambda/types.ts
@@ -12,8 +12,50 @@ export enum LogLevel {
 
 export enum PublicKeyFormat {
   /* eslint-disable @typescript-eslint/naming-convention */
-  OPENSSH = 'OPENSSH',
-  PEM = 'PEM',
+  /**
+   * OpenSSH format
+   */
+  OPENSSH = 'openssh',
+
+  /**
+   * SSH format
+   */
+  SSH = 'ssh',
+
+  /**
+   * PEM format
+   */
+  PEM = 'pem',
+
+  /**
+   * PKCS#1 format
+   */
+  PKCS1 = 'pkcs1',
+
+  /**
+   * PKCS#8 format
+   */
+  PKCS8 = 'pkcs8',
+
+  /**
+   * Raw OpenSSH wire format
+   *
+   * As CloudFormation cannot handle binary data, if the public key is exposed in the template, the value is base64 encoded
+   */
+  RFC4253 = 'rfc4253',
+
+  /**
+   * PuTTY ppk format
+   */
+  PUTTY = 'putty',
+
+  /* eslint-enable @typescript-eslint/naming-convention */
+}
+
+export enum KeyType {
+  /* eslint-disable @typescript-eslint/naming-convention */
+  RSA = 'rsa',
+  ED25519 = 'ed25519',
   /* eslint-enable @typescript-eslint/naming-convention */
 }
 
@@ -27,6 +69,7 @@ export interface ResourceProperties {
   Description: string;
   KmsPrivate: string;
   KmsPublic: string;
+  KeyType: KeyType;
   PublicKeyFormat: PublicKeyFormat;
   RemoveKeySecretsAfterDays: number;
   StackName: string;
diff --git a/lib/index.ts b/lib/index.ts
index 88e76ebf..8b82c0c8 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -16,8 +16,13 @@ import {
 import { IKeyPair, OperatingSystemType } from 'aws-cdk-lib/aws-ec2';
 import { Construct } from 'constructs';
 import * as path from 'path';
-import { LogLevel, PublicKeyFormat, ResourceProperties } from './types';
-export { LogLevel, PublicKeyFormat } from './types';
+import {
+  KeyType,
+  LogLevel,
+  PublicKeyFormat,
+  ResourceProperties,
+} from './types';
+export { KeyType, LogLevel, PublicKeyFormat } from './types';
 
 const resourceType = 'Custom::EC2-Key-Pair';
 const ID = `CFN::Resource::${resourceType}`;
@@ -96,12 +101,19 @@ export interface KeyPairProps extends ResourceProps {
    */
   readonly exposePublicKey?: boolean;
 
+  /**
+   * The type of key pair
+   *
+   * @default - RSA
+   */
+  readonly keyType?: KeyType;
+
   /**
    * Format for public key.
    *
    * Relevant only if the public key is stored and/or exposed.
    *
-   * @default - OPENSSH
+   * @default - SSH
    */
   readonly publicKeyFormat?: PublicKeyFormat;
 
@@ -185,6 +197,11 @@ export class KeyPair extends Resource implements ITaggable, IKeyPair {
    */
   public readonly keyPairID: string = '';
 
+  /**
+   * Type of the Key Pair
+   */
+  public readonly keyType: KeyType;
+
   /**
    * Resource tags
    */
@@ -220,6 +237,15 @@ export class KeyPair extends Resource implements ITaggable, IKeyPair {
       );
     }
 
+    if (
+      props.keyType == KeyType.ED25519 &&
+      props.publicKeyFormat == PublicKeyFormat.PKCS1
+    ) {
+      Annotations.of(this).addError(
+        'The public key format PKCS1 is not supported for key type ED25519',
+      );
+    }
+
     const stack = Stack.of(this).stackName;
 
     if (props.legacyLambdaName) {
@@ -237,6 +263,8 @@ export class KeyPair extends Resource implements ITaggable, IKeyPair {
     this.tags = new TagManager(TagType.MAP, 'Custom::EC2-Key-Pair');
     this.tags.setTag(createdByTag, ID);
 
+    this.keyType = props.keyType ?? KeyType.RSA;
+
     const kmsPrivate = props.kmsPrivateKey ?? props.kms;
     const kmsPublic = props.kmsPublicKey ?? props.kms;
 
@@ -249,7 +277,8 @@ export class KeyPair extends Resource implements ITaggable, IKeyPair {
       PublicKey: props.publicKey ?? '',
       StorePublicKey: props.storePublicKey ? 'true' : 'false',
       ExposePublicKey: props.exposePublicKey ? 'true' : 'false',
-      PublicKeyFormat: props.publicKeyFormat ?? PublicKeyFormat.OPENSSH,
+      KeyType: this.keyType,
+      PublicKeyFormat: props.publicKeyFormat ?? PublicKeyFormat.SSH,
       RemoveKeySecretsAfterDays: props.removeKeySecretsAfterDays ?? 0,
       SecretPrefix: props.secretPrefix ?? 'ec2-ssh-key/',
       StackName: stack,
@@ -378,7 +407,7 @@ export class KeyPair extends Resource implements ITaggable, IKeyPair {
     const fn = new aws_lambda.Function(stack, constructName, {
       functionName: legacyLambdaName ? `${this.prefix}-${cleanID}` : undefined,
       description: 'Custom CFN resource: Manage EC2 Key Pairs',
-      runtime: aws_lambda.Runtime.NODEJS_18_X,
+      runtime: aws_lambda.Runtime.NODEJS_20_X,
       handler: 'index.handler',
       code: aws_lambda.Code.fromAsset(
         path.join(__dirname, '../lambda/code.zip'),
@@ -426,7 +455,12 @@ export class KeyPair extends Resource implements ITaggable, IKeyPair {
    *
    * @internal
    */
-  public _isOsCompatible(_osType: OperatingSystemType): boolean {
-    return true; // as we currently only support OpenSSH, we are compatible with all OS types
+  public _isOsCompatible(osType: OperatingSystemType): boolean {
+    switch (this.keyType) {
+      case KeyType.ED25519:
+        return osType !== OperatingSystemType.WINDOWS;
+      default:
+        return true;
+    }
   }
 }
diff --git a/package-lock.json b/package-lock.json
index b8c33964..27255a8a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
         "@swc/helpers": "0.5.7",
         "@types/node": "20.11.30",
         "@types/node-forge": "1.3.11",
+        "@types/sshpk": "^1.17.4",
         "@typescript-eslint/eslint-plugin": "7.3.1",
         "@typescript-eslint/parser": "7.3.1",
         "aws-cdk-lib": "2.133.0",
@@ -2685,6 +2686,15 @@
       "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
       "dev": true
     },
+    "node_modules/@types/asn1": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/@types/asn1/-/asn1-0.2.4.tgz",
+      "integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/fs-extra": {
       "version": "8.1.5",
       "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz",
@@ -2724,6 +2734,16 @@
       "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
       "dev": true
     },
+    "node_modules/@types/sshpk": {
+      "version": "1.17.4",
+      "resolved": "https://registry.npmjs.org/@types/sshpk/-/sshpk-1.17.4.tgz",
+      "integrity": "sha512-5gI/7eJn6wmkuIuFY8JZJ1g5b30H9K5U5vKrvOuYu+hoZLb2xcVEgxhYZ2Vhbs0w/ACyzyfkJq0hQtBfSCugjw==",
+      "dev": true,
+      "dependencies": {
+        "@types/asn1": "*",
+        "@types/node": "*"
+      }
+    },
     "node_modules/@typescript-eslint/eslint-plugin": {
       "version": "7.3.1",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz",
diff --git a/package.json b/package.json
index e3f1e91e..13afc28a 100644
--- a/package.json
+++ b/package.json
@@ -55,16 +55,17 @@
     }
   },
   "devDependencies": {
+    "@aws-sdk/client-ec2": "3.540.0",
+    "@aws-sdk/client-secrets-manager": "3.540.0",
     "@swc/core": "1.4.8",
     "@swc/helpers": "0.5.7",
     "@types/node": "20.11.30",
     "@types/node-forge": "1.3.11",
+    "@types/sshpk": "^1.17.4",
     "@typescript-eslint/eslint-plugin": "7.3.1",
     "@typescript-eslint/parser": "7.3.1",
     "aws-cdk-lib": "2.133.0",
     "aws-cloudformation-custom-resource": "5.0.0",
-    "@aws-sdk/client-ec2": "3.540.0",
-    "@aws-sdk/client-secrets-manager": "3.540.0",
     "constructs": "10.3.0",
     "eslint": "8.57.0",
     "eslint-config-prettier": "9.1.0",
@@ -73,8 +74,8 @@
     "jsii": "5.3.29",
     "jsii-pacmak": "1.95.0",
     "node-forge": "1.3.1",
-    "publib": "0.2.808",
     "prettier": "3.2.5",
+    "publib": "0.2.808",
     "regenerator-runtime": "0.14.1",
     "ts-node": "10.9.2",
     "typescript": "5.4.3"


Test patch:
diff --git a/test/lib/test-stack.ts b/test/lib/test-stack.ts
index d7881700..78564b07 100644
--- a/test/lib/test-stack.ts
+++ b/test/lib/test-stack.ts
@@ -8,7 +8,7 @@ import {
 } from 'aws-cdk-lib';
 import cloudfront = require('aws-cdk-lib/aws-cloudfront');
 import { Construct } from 'constructs';
-import { LogLevel, PublicKeyFormat } from '../../lambda/types';
+import { KeyType, LogLevel, PublicKeyFormat } from '../../lambda/types';
 import { KeyPair } from '../../lib';
 
 interface Props extends StackProps {
@@ -103,5 +103,31 @@ export class TestStack extends Stack {
     new cloudfront.KeyGroup(this, 'Signing-Key-Group', {
       items: [pubKey],
     });
+
+    for (const [_key, publicKeyFormat] of Object.entries(PublicKeyFormat)) {
+      for (const [_key, keyType] of Object.entries(KeyType)) {
+        if (
+          keyType === KeyType.ED25519 &&
+          publicKeyFormat == PublicKeyFormat.PKCS1
+        ) {
+          // combination not supported
+          continue;
+        }
+
+        const keyPairName = `Test-Key-Pair-${keyType}-${publicKeyFormat}`;
+        const keyPair = new KeyPair(this, keyPairName, {
+          keyPairName,
+          keyType,
+          publicKeyFormat,
+          exposePublicKey: true,
+          storePublicKey: true,
+          logLevel,
+        });
+        new CfnOutput(this, `${keyPairName}-Public-Key`, {
+          exportName: `${keyPairName}-Public-Key`,
+          value: keyPair.publicKeyValue,
+        });
+      }
+    }
   }
 }