Skip to content

Commit 6bd1d78

Browse files
authored
Merge pull request #16 from orlandos-nl/feature/ssh-server
Support an SSH (SFTP) server
2 parents ac8a874 + 13f7f9e commit 6bd1d78

26 files changed

+2611
-484
lines changed

Package.resolved

+10-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
"version": "5.3.0"
1111
}
1212
},
13+
{
14+
"package": "swift-atomics",
15+
"repositoryURL": "https://github.com/apple/swift-atomics.git",
16+
"state": {
17+
"branch": null,
18+
"revision": "919eb1d83e02121cdb434c7bfc1f0c66ef17febe",
19+
"version": "1.0.2"
20+
}
21+
},
1322
{
1423
"package": "swift-crypto",
1524
"repositoryURL": "https://github.com/apple/swift-crypto.git",
@@ -42,7 +51,7 @@
4251
"repositoryURL": "https://github.com/Joannis/swift-nio-ssh.git",
4352
"state": {
4453
"branch": "citadel2",
45-
"revision": "171bb0447d52928b4c49790579c98006e1d4ccd4",
54+
"revision": "ff31fabc505f34abd6886501c112d806a0d75ef6",
4655
"version": null
4756
}
4857
}

Sources/Citadel/Algorithms/AES.swift

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ enum CitadelError: Error {
1414
case invalidSignature
1515
case signingError
1616
case unsupported
17+
case unauthorized
1718
case commandOutputTooLarge
1819
case channelCreationFailed
1920
}

Sources/Citadel/Algorithms/DiffieHellman.swift renamed to Sources/Citadel/Algorithms/DH-Helpers.swift

+16-225
Original file line numberDiff line numberDiff line change
@@ -42,218 +42,9 @@ let dh14p: [UInt8] = [
4242
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
4343
]
4444

45-
public struct DiffieHellmanGroup14Sha1: NIOSSHKeyExchangeAlgorithmProtocol {
46-
public static let keyExchangeInitMessageId: UInt8 = 30
47-
public static let keyExchangeReplyMessageId: UInt8 = 31
48-
49-
public static let keyExchangeAlgorithmNames: [Substring] = ["diffie-hellman-group14-sha1"]
50-
51-
private var previousSessionIdentifier: ByteBuffer?
52-
private var ourRole: SSHConnectionRole
53-
private var theirKey: Insecure.RSA.PublicKey?
54-
private var sharedSecret: Data?
55-
public let ourKey: Insecure.RSA.PrivateKey
56-
public static var ourKey: Insecure.RSA.PrivateKey?
57-
58-
private struct _KeyExchangeResult {
59-
var sessionID: ByteBuffer
60-
var exchangeHash: Insecure.SHA1.Digest
61-
var keys: NIOSSHSessionKeys
62-
}
63-
64-
public init(ourRole: SSHConnectionRole, previousSessionIdentifier: ByteBuffer?) {
65-
self.ourRole = ourRole
66-
self.previousSessionIdentifier = previousSessionIdentifier
67-
self.ourKey = Self.ourKey ?? Insecure.RSA.PrivateKey()
68-
}
69-
70-
public func initiateKeyExchangeClientSide(allocator: ByteBufferAllocator) -> ByteBuffer {
71-
var buffer = allocator.buffer(capacity: 256)
72-
73-
buffer.writeBignum(ourKey._publicKey.modulus)
74-
return buffer
75-
}
76-
77-
public mutating func completeKeyExchangeServerSide(
78-
clientKeyExchangeMessage message: ByteBuffer,
79-
serverHostKey: NIOSSHPrivateKey,
80-
initialExchangeBytes: inout ByteBuffer,
81-
allocator: ByteBufferAllocator,
82-
expectedKeySizes: ExpectedKeySizes
83-
) throws -> (KeyExchangeResult, NIOSSHKeyExchangeServerReply) {
84-
throw CitadelError.unsupported
85-
}
86-
87-
public mutating func receiveServerKeyExchangePayload(serverKeyExchangeMessage: NIOSSHKeyExchangeServerReply, initialExchangeBytes: inout ByteBuffer, allocator: ByteBufferAllocator, expectedKeySizes: ExpectedKeySizes) throws -> KeyExchangeResult {
88-
let kexResult = try self.finalizeKeyExchange(theirKeyBytes: serverKeyExchangeMessage.publicKey,
89-
initialExchangeBytes: &initialExchangeBytes,
90-
serverHostKey: serverKeyExchangeMessage.hostKey,
91-
allocator: allocator,
92-
expectedKeySizes: expectedKeySizes)
93-
94-
// We can now verify signature over the exchange hash.
95-
guard serverKeyExchangeMessage.hostKey.isValidSignature(serverKeyExchangeMessage.signature, for: kexResult.exchangeHash) else {
96-
throw CitadelError.invalidSignature
97-
}
98-
99-
// Great, all done here.
100-
return KeyExchangeResult(
101-
sessionID: kexResult.sessionID,
102-
keys: kexResult.keys
103-
)
104-
}
105-
106-
private mutating func finalizeKeyExchange(theirKeyBytes f: ByteBuffer,
107-
initialExchangeBytes: inout ByteBuffer,
108-
serverHostKey: NIOSSHPublicKey,
109-
allocator: ByteBufferAllocator,
110-
expectedKeySizes: ExpectedKeySizes) throws -> _KeyExchangeResult {
111-
let f = f.getBytes(at: 0, length: f.readableBytes)!
112-
113-
let serverPublicKey = CCryptoBoringSSL_BN_bin2bn(f, f.count, nil)!
114-
defer { CCryptoBoringSSL_BN_free(serverPublicKey) }
115-
let secret = CCryptoBoringSSL_BN_new()!
116-
let serverHostKeyBN = CCryptoBoringSSL_BN_new()
117-
defer { CCryptoBoringSSL_BN_free(serverHostKeyBN) }
118-
119-
var buffer = ByteBuffer()
120-
serverHostKey.write(to: &buffer)
121-
buffer.readWithUnsafeReadableBytes { buffer in
122-
let buffer = buffer.bindMemory(to: UInt8.self)
123-
CCryptoBoringSSL_BN_bin2bn(buffer.baseAddress!, buffer.count, serverHostKeyBN)
124-
return buffer.count
125-
}
126-
127-
let ctx = CCryptoBoringSSL_BN_CTX_new()
128-
defer { CCryptoBoringSSL_BN_CTX_free(ctx) }
129-
130-
let group = CCryptoBoringSSL_BN_bin2bn(dh14p, dh14p.count, nil)
131-
defer { CCryptoBoringSSL_BN_free(group) }
132-
133-
guard CCryptoBoringSSL_BN_mod_exp(
134-
secret,
135-
serverPublicKey,
136-
ourKey.privateExponent,
137-
group,
138-
ctx
139-
) == 1 else {
140-
throw CitadelError.cryptographicError
141-
}
142-
143-
var sharedSecret = [UInt8]()
144-
sharedSecret.reserveCapacity(Int(CCryptoBoringSSL_BN_num_bytes(secret)))
145-
CCryptoBoringSSL_BN_bn2bin(secret, &sharedSecret)
146-
147-
self.sharedSecret = Data(sharedSecret)
148-
149-
func hexEncodedString(array: [UInt8]) -> String {
150-
return array.map { String(format: "%02hhx", $0) }.joined()
151-
}
152-
153-
//var offset = initialExchangeBytes.writerIndex
154-
initialExchangeBytes.writeCompositeSSHString {
155-
serverHostKey.write(to: &$0)
156-
}
157-
158-
//offset = initialExchangeBytes.writerIndex
159-
switch self.ourRole {
160-
case .client:
161-
initialExchangeBytes.writeMPBignum(ourKey._publicKey.modulus)
162-
//offset = initialExchangeBytes.writerIndex
163-
initialExchangeBytes.writeMPBignum(serverPublicKey)
164-
case .server:
165-
initialExchangeBytes.writeMPBignum(serverPublicKey)
166-
initialExchangeBytes.writeMPBignum(ourKey._publicKey.modulus)
167-
}
168-
169-
// Ok, now finalize the exchange hash. If we don't have a previous session identifier at this stage, we do now!
170-
initialExchangeBytes.writeMPBignum(secret)
171-
172-
let exchangeHash = Insecure.SHA1.hash(data: initialExchangeBytes.readableBytesView)
173-
174-
let sessionID: ByteBuffer
175-
if let previousSessionIdentifier = self.previousSessionIdentifier {
176-
sessionID = previousSessionIdentifier
177-
} else {
178-
var hashBytes = allocator.buffer(capacity: Insecure.SHA1.byteCount)
179-
hashBytes.writeContiguousBytes(exchangeHash)
180-
sessionID = hashBytes
181-
}
182-
183-
// Now we can generate the keys.
184-
let keys = self.generateKeys(secret: secret, exchangeHash: exchangeHash, sessionID: sessionID, expectedKeySizes: expectedKeySizes)
185-
186-
// All done!
187-
return _KeyExchangeResult(sessionID: sessionID, exchangeHash: exchangeHash, keys: keys)
188-
}
189-
190-
private func generateKeys(secret: UnsafeMutablePointer<BIGNUM>, exchangeHash: Insecure.SHA1.Digest, sessionID: ByteBuffer, expectedKeySizes: ExpectedKeySizes) -> NIOSSHSessionKeys {
191-
// Cool, now it's time to generate the keys. In my ideal world I'd have a mechanism to handle this digest securely, but this is
192-
// not available in CryptoKit so we're going to spill these keys all over the heap and the stack. This isn't ideal, but I don't
193-
// think the risk is too bad.
194-
//
195-
// We generate these as follows:
196-
//
197-
// - Initial IV client to server: HASH(K || H || "A" || session_id)
198-
// (Here K is encoded as mpint and "A" as byte and session_id as raw
199-
// data. "A" means the single character A, ASCII 65).
200-
// - Initial IV server to client: HASH(K || H || "B" || session_id)
201-
// - Encryption key client to server: HASH(K || H || "C" || session_id)
202-
// - Encryption key server to client: HASH(K || H || "D" || session_id)
203-
// - Integrity key client to server: HASH(K || H || "E" || session_id)
204-
// - Integrity key server to client: HASH(K || H || "F" || session_id)
205-
206-
func calculateSha1SymmetricKey(letter: UInt8, expectedKeySize size: Int) -> SymmetricKey {
207-
SymmetricKey(data: calculateSha1Key(letter: letter, expectedKeySize: size))
208-
}
209-
210-
func calculateSha1Key(letter: UInt8, expectedKeySize size: Int) -> [UInt8] {
211-
var result = [UInt8]()
212-
var hashInput = ByteBuffer()
213-
214-
while result.count < size {
215-
hashInput.moveWriterIndex(to: 0)
216-
hashInput.writeMPBignum(secret)
217-
hashInput.writeBytes(exchangeHash)
218-
219-
if !result.isEmpty {
220-
hashInput.writeBytes(result)
221-
} else {
222-
hashInput.writeInteger(letter)
223-
hashInput.writeBytes(sessionID.readableBytesView)
224-
}
225-
226-
result += Insecure.SHA1.hash(data: hashInput.readableBytesView)
227-
}
228-
229-
result.removeLast(result.count - size)
230-
return result
231-
}
232-
233-
switch self.ourRole {
234-
case .client:
235-
return NIOSSHSessionKeys(
236-
initialInboundIV: calculateSha1Key(letter: UInt8(ascii: "B"), expectedKeySize: expectedKeySizes.ivSize),
237-
initialOutboundIV: calculateSha1Key(letter: UInt8(ascii: "A"), expectedKeySize: expectedKeySizes.ivSize),
238-
inboundEncryptionKey: calculateSha1SymmetricKey(letter: UInt8(ascii: "D"), expectedKeySize: expectedKeySizes.encryptionKeySize),
239-
outboundEncryptionKey: calculateSha1SymmetricKey(letter: UInt8(ascii: "C"), expectedKeySize: expectedKeySizes.encryptionKeySize),
240-
inboundMACKey: calculateSha1SymmetricKey(letter: UInt8(ascii: "F"), expectedKeySize: expectedKeySizes.macKeySize),
241-
outboundMACKey: calculateSha1SymmetricKey(letter: UInt8(ascii: "E"), expectedKeySize: expectedKeySizes.macKeySize))
242-
case .server:
243-
return NIOSSHSessionKeys(
244-
initialInboundIV: calculateSha1Key(letter: UInt8(ascii: "A"), expectedKeySize: expectedKeySizes.ivSize),
245-
initialOutboundIV: calculateSha1Key(letter: UInt8(ascii: "B"), expectedKeySize: expectedKeySizes.ivSize),
246-
inboundEncryptionKey: calculateSha1SymmetricKey(letter: UInt8(ascii: "C"), expectedKeySize: expectedKeySizes.encryptionKeySize),
247-
outboundEncryptionKey: calculateSha1SymmetricKey(letter: UInt8(ascii: "D"), expectedKeySize: expectedKeySizes.encryptionKeySize),
248-
inboundMACKey: calculateSha1SymmetricKey(letter: UInt8(ascii: "E"), expectedKeySize: expectedKeySizes.macKeySize),
249-
outboundMACKey: calculateSha1SymmetricKey(letter: UInt8(ascii: "F"), expectedKeySize: expectedKeySizes.macKeySize))
250-
}
251-
}
252-
}
253-
25445
extension SymmetricKey {
25546
/// Creates a symmetric key by truncating a given digest.
256-
fileprivate static func truncatingDigest<D: Digest>(_ digest: D, length: Int) -> SymmetricKey {
47+
static func truncatingDigest<D: Digest>(_ digest: D, length: Int) -> SymmetricKey {
25748
assert(length <= D.byteCount)
25849
return digest.withUnsafeBytes { bodyPtr in
25950
SymmetricKey(data: UnsafeRawBufferPointer(rebasing: bodyPtr.prefix(length)))
@@ -262,7 +53,7 @@ extension SymmetricKey {
26253
}
26354

26455
extension HashFunction {
265-
fileprivate mutating func update(byte: UInt8) {
56+
mutating func update(byte: UInt8) {
26657
withUnsafeBytes(of: byte) { bytePtr in
26758
assert(bytePtr.count == 1, "Why is this 8 bit integer so large?")
26859
self.update(bufferPointer: bytePtr)
@@ -299,7 +90,7 @@ extension ByteBuffer {
29990
var size = (bignum.bitWidth + 7) / 8
30091
writeWithUnsafeMutableBytes(minimumWritableBytes: Int(size + 1)) { buffer in
30192
let buffer = buffer.bindMemory(to: UInt8.self)
302-
93+
30394
buffer.baseAddress!.pointee = 0
30495

30596
let serialized = Array(bignum.serialize())
@@ -356,7 +147,7 @@ extension HashFunction {
356147
fileprivate mutating func updateAsMPInt(sharedSecret: Data) {
357148
sharedSecret.withUnsafeBytes { secretBytesPtr in
358149
var secretBytesPtr = secretBytesPtr[...]
359-
150+
360151
// Here we treat this shared secret as an mpint by just treating these bytes as an unsigned
361152
// fixed-length integer in network byte order, as suggested by draft-ietf-curdle-ssh-curves-08,
362153
// and "prepending" it with a 32-bit length field. Note that instead of prepending, we just make
@@ -385,11 +176,11 @@ extension HashFunction {
385176
}
386177
let numberOfZeroBytes = firstNonZeroByteIndex - secretBytesPtr.startIndex
387178
let topBitOfFirstNonZeroByteIsSet = secretBytesPtr[firstNonZeroByteIndex] & 0x80 == 0x80
388-
179+
389180
// We need to hash a few extra bytes: specifically, we need a 4 byte length in network byte order,
390181
// and maybe a fifth as a zero byte.
391182
var lengthHelper = SharedSecretLengthHelper()
392-
183+
393184
switch (numberOfZeroBytes, topBitOfFirstNonZeroByteIsSet) {
394185
case (0, false):
395186
// This is the easy case, we just treat the whole thing as the body.
@@ -410,7 +201,7 @@ extension HashFunction {
410201
lengthHelper.length = UInt8(secretBytesPtr.count)
411202
lengthHelper.useExtraZeroByte = false
412203
}
413-
204+
414205
// Now generate the hash.
415206
lengthHelper.update(hasher: &self)
416207
self.update(bufferPointer: UnsafeRawBufferPointer(rebasing: secretBytesPtr))
@@ -429,10 +220,10 @@ private struct SharedSecretLengthHelper {
429220
// 32 bytes long (before the mpint transformation), we only ever actually need to modify one of these bytes:
430221
// the 4th.
431222
private var backingBytes = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0))
432-
223+
433224
/// Whether we should hash an extra zero byte.
434225
var useExtraZeroByte: Bool = false
435-
226+
436227
/// The length to encode.
437228
var length: UInt8 {
438229
get {
@@ -442,27 +233,27 @@ private struct SharedSecretLengthHelper {
442233
self.backingBytes.3 = newValue
443234
}
444235
}
445-
236+
446237
// Remove the elementwise initializer.
447238
init() {}
448-
239+
449240
func update<Hasher: HashFunction>(hasher: inout Hasher) {
450241
withUnsafeBytes(of: self.backingBytes) { bytesPtr in
451242
precondition(bytesPtr.count == 5)
452-
243+
453244
let bytesToHash: UnsafeRawBufferPointer
454245
if self.useExtraZeroByte {
455246
bytesToHash = bytesPtr
456247
} else {
457248
bytesToHash = UnsafeRawBufferPointer(rebasing: bytesPtr.prefix(4))
458249
}
459-
250+
460251
hasher.update(bufferPointer: bytesToHash)
461252
}
462253
}
463254
}
464255

465-
fileprivate extension ByteBuffer {
256+
extension ByteBuffer {
466257
/// Many functions in SSH write composite data structures into an SSH string. This is a tricky thing to express
467258
/// without confining all of those functions to writing strings directly, which is pretty uncool. Instead, we can
468259
/// wrap the body into this function, which will take the returned total length and use that as the string length.
@@ -471,7 +262,7 @@ fileprivate extension ByteBuffer {
471262
// Reserve 4 bytes for the length.
472263
let originalWriterIndex = self.writerIndex
473264
self.moveWriterIndex(forwardBy: 4)
474-
265+
475266
var writtenLength: Int
476267
do {
477268
writtenLength = try compositeFunction(&self)
@@ -480,7 +271,7 @@ fileprivate extension ByteBuffer {
480271
self.moveWriterIndex(to: originalWriterIndex)
481272
throw error
482273
}
483-
274+
484275
// Ok, now we're going to write the length.
485276
writtenLength += self.setInteger(UInt32(writtenLength), at: originalWriterIndex)
486277
return writtenLength

0 commit comments

Comments
 (0)