Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Legend:
| public-key | `ecdsa-sha2-nistp256` | ✔✔
| public-key | `ecdsa-sha2-nistp384` | ✔✔
| public-key | `ecdsa-sha2-nistp521` | ✔
| public-key | `ssh-ed25519` | ?? [1]
| public-key | `ssh-ed25519` | [4]
| public-key | `*-cert-v01@openssh.com` | ?? [2]
| | |
| cipher | `aes256-cbc` | ✔✔ [3]
Expand All @@ -137,11 +137,15 @@ Legend:

[1] May require use of 3rd-party libs, though Curve25519 APIs are under
consideration for [.NET](https://github.com/dotnet/runtime/issues/14741) and
[web crypto](https://github.com/w3c/webcrypto/issues/233).
[web crypto](https://github.com/w3c/webcrypto/issues/233).
Ed25519 is now supported in the TS library (see [4]).
[2] OpenSSH certificate support should be possible with some work.
[3] AES-CBC is not supported in browsers due to a [limitation](
https://github.com/w3c/webcrypto/issues/73) of the web crypto API. AES-CTR or
AES-GCM works fine.
AES-GCM works fine.
[4] Ed25519 is supported in the TypeScript library only (Node.js and browsers
via web crypto). Not yet enabled in the default session configuration; add
`SshAlgorithms.publicKey.ed25519` to enable it.

There is no plan to have built-in support for older algorithms known to be
insecure (for example SHA-1), though in some cases these can be easily added by
Expand All @@ -156,10 +160,10 @@ or [src/ts/ssh-keys/README.md](src/ts/ssh-keys/README.md).

| Key Format | Key Algorithm | Password Protection | Format Description |
| -------------------- | ------------- | ------------------- | ------------------ |
| SSH public key | RSA<br>ECDSA | N/A | Single line key algorithm name, base64-encoded key bytes, and optional comment. Files conventionally end with `.pub`.
| SSH public key | RSA<br>ECDSA<br>Ed25519 | N/A | Single line key algorithm name, base64-encoded key bytes, and optional comment. Files conventionally end with `.pub`.
| PKCS#1 | RSA | _import&nbsp;only_ | Starts with one of:<br>`-----BEGIN RSA PUBLIC KEY-----`<br>`-----BEGIN RSA PRIVATE KEY-----`
| SEC1 | ECDSA | _import&nbsp;only_ | Starts with:<br>`-----BEGIN EC PRIVATE KEY-----`
| PKCS#8 | RSA<br>ECDSA | ✔ | Starts with one of:<br>`-----BEGIN PUBLIC KEY-----`<br>`-----BEGIN PRIVATE KEY-----`<br>`-----BEGIN ENCRYPTED PRIVATE KEY-----`
| PKCS#8 | RSA<br>ECDSA<br>Ed25519 | ✔ | Starts with one of:<br>`-----BEGIN PUBLIC KEY-----`<br>`-----BEGIN PRIVATE KEY-----`<br>`-----BEGIN ENCRYPTED PRIVATE KEY-----`
| SSH2<br>_C# only_ | RSA | ✔ | Starts with one of:<br>`---- BEGIN SSH2 PUBLIC KEY ----`<br>`---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----`
| OpenSSH<br>_C# only_ | RSA<br>ECDSA | ✔ | Starts with one of:<br>`-----BEGIN OPENSSH PUBLIC KEY-----`<br>`-----BEGIN OPENSSH PRIVATE KEY-----`
| JWK<br>_TS only_ | RSA<br>ECDSA | N/A | JSON with key algorithm name and parameters
Expand All @@ -176,3 +180,4 @@ The following RFCs define the SSH protocol:
- [RFC 5647 - AES GCM for the SSH Protocol](https://tools.ietf.org/html/rfc5647)
- [RFC 5656 - EC Algorithm Integration in SSH](https://tools.ietf.org/html/rfc5656)
- [RFC 8308 - SSH Extension Negotiation](https://tools.ietf.org/html/rfc8308)
- [RFC 8709 - Ed25519 and Ed448 Public Key Algorithms for SSH](https://tools.ietf.org/html/rfc8709)
85 changes: 85 additions & 0 deletions src/ts/ssh-keys/pkcs8KeyFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import {
Random,
ECParameters,
ECDsa,
Ed25519,
EdDSAParameters,
} from '@microsoft/dev-tunnels-ssh';
import { KeyFormatter, useWebCrypto } from './keyFormatter';
import { KeyData } from './keyData';

const enum Oids {
rsa = '1.2.840.113549.1.1.1',
ec = '1.2.840.10045.2.1',
ed25519 = '1.3.101.112',
pkcs5PBKDF2 = '1.2.840.113549.1.5.12',
pkcs5PBES2 = '1.2.840.113549.1.5.13',
hmacWithSHA256 = '1.2.840.113549.2.9',
Expand Down Expand Up @@ -54,10 +57,12 @@ export class Pkcs8KeyFormatter implements KeyFormatter {
public constructor() {
this.importers.set(Oids.rsa, Pkcs8KeyFormatter.importRsaKey);
this.importers.set(Oids.ec, Pkcs8KeyFormatter.importECKey);
this.importers.set(Oids.ed25519, Pkcs8KeyFormatter.importEd25519Key);
this.exporters.set(Rsa.keyAlgorithmName, Pkcs8KeyFormatter.exportRsaKey);
this.exporters.set(ECDsa.ecdsaSha2Nistp256, Pkcs8KeyFormatter.exportECKey);
this.exporters.set(ECDsa.ecdsaSha2Nistp384, Pkcs8KeyFormatter.exportECKey);
this.exporters.set(ECDsa.ecdsaSha2Nistp521, Pkcs8KeyFormatter.exportECKey);
this.exporters.set(Ed25519.keyAlgorithmName, Pkcs8KeyFormatter.exportEd25519Key);
}

/** Mapping from public key algorithm OID to import handler for that algorithm. */
Expand Down Expand Up @@ -376,6 +381,86 @@ export class Pkcs8KeyFormatter implements KeyFormatter {
}
}

private static async importEd25519Key(
keyBytes: Buffer,
oidReader: DerReader,
includePrivate: boolean,
): Promise<KeyPair> {
let publicKeyBytes: Buffer;
let privateKeyBytes: Buffer | undefined;

if (includePrivate) {
// The private key is wrapped in an OCTET STRING containing the raw 32-byte key.
const keyReader = new DerReader(keyBytes);
privateKeyBytes = keyReader.readOctetString();

// For PKCS#8 Ed25519 private keys, the public key may not be present
// in the inner structure. We need to derive it or it may be appended.
// Some encoders append the public key after the private key bytes.
if (privateKeyBytes.length === 64) {
// Some formats store private + public concatenated.
publicKeyBytes = privateKeyBytes.slice(32);
privateKeyBytes = privateKeyBytes.slice(0, 32);
} else if (privateKeyBytes.length === 32) {
// Generate public key from private key by importing and re-exporting.
// For now, create a temporary key pair to derive the public key.
const tempKeyPair = new Ed25519.KeyPair();
const tempParams: EdDSAParameters = {
curve: { name: 'Ed25519' },
publicKey: Buffer.alloc(32), // placeholder
privateKey: privateKeyBytes,
};
// We can't derive the public key without importing. Import with a
// placeholder and let the key pair handle it via generate + re-import.
// Instead, use the raw PKCS#8 bytes directly.
await tempKeyPair.importParameters(tempParams);
const exported = <EdDSAParameters>await tempKeyPair.exportParameters();
publicKeyBytes = exported.publicKey;
} else {
throw new Error(`Unexpected Ed25519 private key length: ${privateKeyBytes.length}`);
}
} else {
publicKeyBytes = keyBytes;
}

if (publicKeyBytes.length !== 32) {
throw new Error(`Unexpected Ed25519 public key length: ${publicKeyBytes.length}`);
}

const parameters: EdDSAParameters = {
curve: { name: 'Ed25519' },
publicKey: publicKeyBytes,
privateKey: privateKeyBytes,
};

const keyPair = new Ed25519.KeyPair();
await keyPair.importParameters(parameters);
return keyPair;
}

private static async exportEd25519Key(
keyPair: KeyPair,
oidWriter: DerWriter,
includePrivate: boolean,
): Promise<Buffer> {
const parameters = <EdDSAParameters>await keyPair.exportParameters();

oidWriter.writeObjectIdentifier(Oids.ed25519);

if (includePrivate) {
if (!parameters.privateKey) {
throw new Error('Missing private key parameters.');
}

// Wrap the raw private key in an OCTET STRING.
const keyWriter = new DerWriter(Buffer.alloc(64));
keyWriter.writeOctetString(parameters.privateKey);
return keyWriter.toBuffer();
} else {
return parameters.publicKey;
}
}

private static async decryptPrivate(keyData: KeyData, passphrase: string): Promise<KeyData> {
let reader = new DerReader(keyData.data);
const innerReader = reader.readSequence();
Expand Down
4 changes: 3 additions & 1 deletion src/ts/ssh-keys/publicKeyFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
//

import { SshAlgorithms, KeyPair, Rsa, ECDsa, SshDataReader } from '@microsoft/dev-tunnels-ssh';
import { SshAlgorithms, KeyPair, Rsa, ECDsa, Ed25519, SshDataReader } from '@microsoft/dev-tunnels-ssh';
import { KeyFormatter } from './keyFormatter';
import { KeyData } from './keyData';

Expand Down Expand Up @@ -31,6 +31,8 @@ export class PublicKeyFormatter implements KeyFormatter {
keyPair = SshAlgorithms.publicKey.ecdsaSha2Nistp384!.createKeyPair();
} else if (keyData.keyType === ECDsa.ecdsaSha2Nistp521) {
keyPair = SshAlgorithms.publicKey.ecdsaSha2Nistp521!.createKeyPair();
} else if (keyData.keyType === Ed25519.keyAlgorithmName) {
keyPair = SshAlgorithms.publicKey.ed25519!.createKeyPair();
}

if (keyPair) {
Expand Down
Loading