Starting work on Implementing SSH Server Service
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
'use strict'
|
||||
|
||||
const bareCrypto = require('bare-crypto')
|
||||
const { Ber } = require('asn1')
|
||||
const { Buffer: Asn1Buffer } = require('safer-buffer')
|
||||
const { getCiphers, getHashes } = require('./lists.js')
|
||||
|
||||
const nextTick =
|
||||
typeof queueMicrotask === 'function'
|
||||
? (fn) => queueMicrotask(fn)
|
||||
: (fn) => setTimeout(fn, 0)
|
||||
|
||||
/**
|
||||
* Encode Ed25519 raw keys as SPKI / PKCS#8 DER (same layout as Node/OpenSSL RFC 8410),
|
||||
* which bare-ssh2 `lib/keygen.js` parses via `convertKeys`.
|
||||
*/
|
||||
function ed25519RawToDer(pub32, privSeed32) {
|
||||
const pub = Asn1Buffer.from(pub32)
|
||||
const priv = Asn1Buffer.from(privSeed32)
|
||||
|
||||
const pubWriter = new Ber.Writer()
|
||||
pubWriter.startSequence()
|
||||
pubWriter.startSequence()
|
||||
pubWriter.writeOID('1.3.101.112')
|
||||
pubWriter.endSequence()
|
||||
pubWriter.startSequence(Ber.BitString)
|
||||
pubWriter.writeByte(0x00)
|
||||
pubWriter._ensure(pub.length)
|
||||
pubWriter._buf.set(pub, pubWriter._offset)
|
||||
pubWriter._offset += pub.length
|
||||
pubWriter.endSequence()
|
||||
pubWriter.endSequence()
|
||||
|
||||
const privWriter = new Ber.Writer()
|
||||
privWriter.startSequence()
|
||||
privWriter.writeInt(0x00, Ber.Integer)
|
||||
privWriter.startSequence()
|
||||
privWriter.writeOID('1.3.101.112')
|
||||
privWriter.endSequence()
|
||||
privWriter.startSequence(Ber.OctetString)
|
||||
privWriter.writeBuffer(priv, Ber.OctetString)
|
||||
privWriter.endSequence()
|
||||
privWriter.endSequence()
|
||||
|
||||
return { publicKey: pubWriter.buffer, privateKey: privWriter.buffer }
|
||||
}
|
||||
|
||||
function generateKeyPairSyncBareEd25519(type, options) {
|
||||
const pubEnc = (options && options.publicKeyEncoding) || {}
|
||||
const privEnc = (options && options.privateKeyEncoding) || {}
|
||||
if (
|
||||
String(type).toLowerCase() !== 'ed25519' ||
|
||||
pubEnc.type !== 'spki' ||
|
||||
pubEnc.format !== 'der' ||
|
||||
privEnc.type !== 'pkcs8' ||
|
||||
privEnc.format !== 'der'
|
||||
) {
|
||||
throw new Error(
|
||||
'Bare crypto shim: generateKeyPairSync only supports ed25519 with { spki, der } / { pkcs8, der } encodings'
|
||||
)
|
||||
}
|
||||
const { publicKey, privateKey } = bareCrypto.generateKeyPair('ed25519')
|
||||
const pubRaw = publicKey._key
|
||||
const privRaw = privateKey._key
|
||||
const pub32 = Asn1Buffer.from(pubRaw)
|
||||
let priv32 = Asn1Buffer.from(privRaw)
|
||||
if (priv32.length === 64) priv32 = Asn1Buffer.from(priv32.subarray(0, 32))
|
||||
if (pub32.length !== 32 || priv32.length !== 32) {
|
||||
throw new Error('Unexpected Ed25519 key material length from bare-crypto')
|
||||
}
|
||||
return ed25519RawToDer(pub32, priv32)
|
||||
}
|
||||
|
||||
function generateKeyPairSync(type, options) {
|
||||
if (typeof bareCrypto.generateKeyPairSync === 'function') {
|
||||
return bareCrypto.generateKeyPairSync(type, options)
|
||||
}
|
||||
const t = String(type || '').toLowerCase()
|
||||
if (t === 'x25519') {
|
||||
const x = require('./x25519-compat.js')
|
||||
if (options && Object.keys(options).length > 0) {
|
||||
const pubEnc = options.publicKeyEncoding || {}
|
||||
const privEnc = options.privateKeyEncoding || {}
|
||||
if (
|
||||
pubEnc.type !== 'spki' ||
|
||||
pubEnc.format !== 'der' ||
|
||||
privEnc.type !== 'pkcs8' ||
|
||||
privEnc.format !== 'der'
|
||||
) {
|
||||
throw new Error(
|
||||
'Bare crypto shim: x25519 generateKeyPairSync only supports default or spki/pkcs8 der'
|
||||
)
|
||||
}
|
||||
}
|
||||
return x.generateKeyPairSync()
|
||||
}
|
||||
if (t === 'ed25519') return generateKeyPairSyncBareEd25519(type, options)
|
||||
throw new Error(`Bare crypto shim: generateKeyPairSync not supported for type ${type}`)
|
||||
}
|
||||
|
||||
function generateKeyPair(type, options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options
|
||||
options = undefined
|
||||
}
|
||||
const native = bareCrypto.generateKeyPair
|
||||
if (typeof callback === 'function') {
|
||||
if (typeof native === 'function' && native.length >= 3) {
|
||||
return native.call(bareCrypto, type, options, callback)
|
||||
}
|
||||
nextTick(() => {
|
||||
try {
|
||||
const pair = generateKeyPairSync(type, options)
|
||||
callback(null, pair.publicKey, pair.privateKey)
|
||||
} catch (err) {
|
||||
callback(err)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
if (typeof native === 'function') {
|
||||
return native.call(bareCrypto, type, options)
|
||||
}
|
||||
}
|
||||
|
||||
function createPublicKey(descriptor) {
|
||||
if (
|
||||
descriptor &&
|
||||
descriptor.type === 'spki' &&
|
||||
descriptor.format === 'der' &&
|
||||
descriptor.key
|
||||
) {
|
||||
return require('./x25519-compat.js').createPublicKeyFromSpkiDer(descriptor.key)
|
||||
}
|
||||
if (typeof bareCrypto.createPublicKey === 'function') {
|
||||
return bareCrypto.createPublicKey(descriptor)
|
||||
}
|
||||
throw new Error('Bare crypto shim: createPublicKey only supports X25519 SPKI DER on Bare')
|
||||
}
|
||||
|
||||
function diffieHellman(opts) {
|
||||
return require('./x25519-compat.js').diffieHellman(opts)
|
||||
}
|
||||
|
||||
function createECDH(curveName) {
|
||||
throw new Error(
|
||||
`Bare crypto shim: createECDH(${curveName}) is not implemented; bare-ssh2 should negotiate curve25519-sha256`
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = Object.assign({}, bareCrypto, {
|
||||
getCiphers,
|
||||
getHashes,
|
||||
generateKeyPairSync,
|
||||
generateKeyPair,
|
||||
createPublicKey,
|
||||
diffieHellman,
|
||||
createECDH
|
||||
})
|
||||
@@ -0,0 +1,153 @@
|
||||
'use strict'
|
||||
|
||||
const { Ber } = require('asn1')
|
||||
const { Buffer: Asn1Buffer } = require('safer-buffer')
|
||||
const { Buffer: RuntimeBuffer } = require('buffer')
|
||||
|
||||
const X25519_OID = '1.3.101.110'
|
||||
|
||||
function getNacl() {
|
||||
return require('tweetnacl')
|
||||
}
|
||||
|
||||
/** @param {import('safer-buffer').Buffer | Uint8Array} buf */
|
||||
function u8(buf) {
|
||||
const b = Asn1Buffer.from(buf)
|
||||
return new Uint8Array(b.buffer, b.byteOffset, b.byteLength)
|
||||
}
|
||||
|
||||
function berSpkiX25519(pub32) {
|
||||
const pub = Asn1Buffer.from(pub32)
|
||||
const w = new Ber.Writer()
|
||||
w.startSequence()
|
||||
w.startSequence()
|
||||
w.writeOID(X25519_OID)
|
||||
w.endSequence()
|
||||
w.startSequence(Ber.BitString)
|
||||
w.writeByte(0x00)
|
||||
w._ensure(pub.length)
|
||||
w._buf.set(pub, w._offset)
|
||||
w._offset += pub.length
|
||||
w.endSequence()
|
||||
w.endSequence()
|
||||
return w.buffer
|
||||
}
|
||||
|
||||
function berPkcs8X25519(priv32) {
|
||||
const priv = Asn1Buffer.from(priv32)
|
||||
const w = new Ber.Writer()
|
||||
w.startSequence()
|
||||
w.writeInt(0x00, Ber.Integer)
|
||||
w.startSequence()
|
||||
w.writeOID(X25519_OID)
|
||||
w.endSequence()
|
||||
w.startSequence(Ber.OctetString)
|
||||
w.writeBuffer(priv, Ber.OctetString)
|
||||
w.endSequence()
|
||||
w.endSequence()
|
||||
return w.buffer
|
||||
}
|
||||
|
||||
/** Match bare-ssh2 kex: SPKI DER ends with 32-byte X25519 public key. */
|
||||
function parseSpkiX25519PubDer(der) {
|
||||
const b = Asn1Buffer.from(der)
|
||||
if (b.length < 32) throw new Error('Invalid X25519 SPKI DER')
|
||||
return u8(b.slice(-32))
|
||||
}
|
||||
|
||||
function parsePkcs8X25519PrivDer(der) {
|
||||
const { Ber: BerR } = require('asn1')
|
||||
let reader = new BerR.Reader(Asn1Buffer.from(der))
|
||||
reader.readSequence()
|
||||
if (reader.readInt() !== 0) throw new Error('Invalid PKCS#8 version')
|
||||
reader.readSequence()
|
||||
if (reader.readOID() !== X25519_OID) throw new Error('Invalid X25519 PKCS#8 OID')
|
||||
reader = new BerR.Reader(reader.readString(BerR.OctetString, true))
|
||||
return u8(reader.readString(BerR.OctetString, true))
|
||||
}
|
||||
|
||||
class X25519PublicKey {
|
||||
/** @param {Uint8Array | import('safer-buffer').Buffer} pub32 */
|
||||
constructor(pub32) {
|
||||
this.type = 'public'
|
||||
this.asymmetricKeyType = 'x25519'
|
||||
this._pub = Asn1Buffer.from(pub32)
|
||||
if (this._pub.length !== 32) throw new Error('X25519 public key must be 32 bytes')
|
||||
}
|
||||
|
||||
/** @param {{ type: string, format: string }} opts */
|
||||
export(opts) {
|
||||
if (!opts || opts.format !== 'der') throw new Error('unsupported export format')
|
||||
if (opts.type === 'spki') return berSpkiX25519(this._pub)
|
||||
if (opts.type === 'raw') return Asn1Buffer.from(this._pub)
|
||||
throw new Error('unsupported export type')
|
||||
}
|
||||
}
|
||||
|
||||
class X25519PrivateKey {
|
||||
/**
|
||||
* @param {Uint8Array | import('safer-buffer').Buffer} priv32
|
||||
* @param {Uint8Array | import('safer-buffer').Buffer | null} pub32
|
||||
*/
|
||||
constructor(priv32, pub32) {
|
||||
this.type = 'private'
|
||||
this.asymmetricKeyType = 'x25519'
|
||||
this._priv = Asn1Buffer.from(priv32)
|
||||
if (this._priv.length !== 32) throw new Error('X25519 private key must be 32 bytes')
|
||||
this._pub = pub32 ? Asn1Buffer.from(pub32) : null
|
||||
}
|
||||
|
||||
/** @param {{ type: string, format: string }} opts */
|
||||
export(opts) {
|
||||
if (!opts || opts.format !== 'der') throw new Error('unsupported export format')
|
||||
if (opts.type === 'pkcs8') return berPkcs8X25519(this._priv)
|
||||
throw new Error('unsupported export type')
|
||||
}
|
||||
}
|
||||
|
||||
function generateKeyPairSync() {
|
||||
const nacl = getNacl()
|
||||
const pair = nacl.box.keyPair()
|
||||
return {
|
||||
publicKey: new X25519PublicKey(pair.publicKey),
|
||||
privateKey: new X25519PrivateKey(pair.secretKey, pair.publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import('safer-buffer').Buffer} der */
|
||||
function createPublicKeyFromSpkiDer(der) {
|
||||
return new X25519PublicKey(parseSpkiX25519PubDer(der))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ privateKey: unknown, publicKey: unknown }} opts
|
||||
* @returns {import('safer-buffer').Buffer}
|
||||
*/
|
||||
function diffieHellman(opts) {
|
||||
const nacl = getNacl()
|
||||
let privU8
|
||||
let pubU8
|
||||
const sk = opts.privateKey
|
||||
const pk = opts.publicKey
|
||||
if (sk instanceof X25519PrivateKey) privU8 = u8(sk._priv)
|
||||
else if (sk && typeof sk.export === 'function')
|
||||
privU8 = parsePkcs8X25519PrivDer(sk.export({ type: 'pkcs8', format: 'der' }))
|
||||
else throw new Error('diffieHellman: unsupported private key')
|
||||
|
||||
if (pk instanceof X25519PublicKey) pubU8 = u8(pk._pub)
|
||||
else if (pk && typeof pk.export === 'function')
|
||||
pubU8 = parseSpkiX25519PubDer(pk.export({ type: 'spki', format: 'der' }))
|
||||
else throw new Error('diffieHellman: unsupported public key')
|
||||
|
||||
const shared = nacl.scalarMult(privU8, pubU8)
|
||||
// Use runtime `buffer` (Bare node-buffer) so kex `convertToMpint` / `hash.update` agree on `Buffer.isBuffer`.
|
||||
return RuntimeBuffer.from(shared)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateKeyPairSync,
|
||||
createPublicKeyFromSpkiDer,
|
||||
diffieHellman,
|
||||
X25519PublicKey,
|
||||
X25519PrivateKey
|
||||
}
|
||||
Reference in New Issue
Block a user