17.12
This commit is contained in:
@@ -0,0 +1,571 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_keys -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
# pylint: disable=I0011,C0103,W9401,W9402
|
||||
|
||||
"""
|
||||
Data used by test_keys as well as others.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from twisted.python.compat import long, _b64decodebytes as decodebytes
|
||||
|
||||
RSAData = {
|
||||
'n': long('269413617238113438198661010376758399219880277968382122687862697'
|
||||
'296942471209955603071120391975773283844560230371884389952067978'
|
||||
'789684135947515341209478065209455427327369102356204259106807047'
|
||||
'964139525310539133073743116175821417513079706301100600025815509'
|
||||
'786721808719302671068052414466483676821987505720384645561708425'
|
||||
'794379383191274856941628512616355437197560712892001107828247792'
|
||||
'561858327085521991407807015047750218508971611590850575870321007'
|
||||
'991909043252470730134547038841839367764074379439843108550888709'
|
||||
'430958143271417044750314742880542002948053835745429446485015316'
|
||||
'60749404403945254975473896534482849256068133525751'),
|
||||
'e': long(65537),
|
||||
'd': long('420335724286999695680502438485489819800002417295071059780489811'
|
||||
'840828351636754206234982682752076205397047218449504537476523960'
|
||||
'987613148307573487322720481066677105211155388802079519869249746'
|
||||
'774085882219244493290663802569201213676433159425782937159766786'
|
||||
'329742053214957933941260042101377175565683849732354700525628975'
|
||||
'239000548651346620826136200952740446562751690924335365940810658'
|
||||
'931238410612521441739702170503547025018016868116037053013935451'
|
||||
'477930426013703886193016416453215950072147440344656137718959053'
|
||||
'897268663969428680144841987624962928576808352739627262941675617'
|
||||
'7724661940425316604626522633351193810751757014073'),
|
||||
'p': long('152689878451107675391723141129365667732639179427453246378763774'
|
||||
'448531436802867910180261906924087589684175595016060014593521649'
|
||||
'964959248408388984465569934780790357826811592229318702991401054'
|
||||
'226302790395714901636384511513449977061729214247279176398290513'
|
||||
'085108930550446985490864812445551198848562639933888780317'),
|
||||
'q': long('176444974592327996338888725079951900172097062203378367409936859'
|
||||
'072670162290963119826394224277287608693818012745872307600855894'
|
||||
'647300295516866118620024751601329775653542084052616260193174546'
|
||||
'400544176890518564317596334518015173606460860373958663673307503'
|
||||
'231977779632583864454001476729233959405710696795574874403'),
|
||||
'u': long('936018002388095842969518498561007090965136403384715613439364803'
|
||||
'229386793506402222847415019772053080458257034241832795210460612'
|
||||
'924445085372678524176842007912276654532773301546269997020970818'
|
||||
'155956828553418266110329867222673040098885651348225673298948529'
|
||||
'93885224775891490070400861134282266967852120152546563278')
|
||||
}
|
||||
|
||||
DSAData = {
|
||||
'g': long("10253261326864117157640690761723586967382334319435778695"
|
||||
"29171533815411392477819921538350732400350395446211982054"
|
||||
"96512489289702949127531056893725702005035043292195216541"
|
||||
"11525058911428414042792836395195432445511200566318251789"
|
||||
"10575695836669396181746841141924498545494149998282951407"
|
||||
"18645344764026044855941864175"),
|
||||
'p': long("10292031726231756443208850082191198787792966516790381991"
|
||||
"77502076899763751166291092085666022362525614129374702633"
|
||||
"26262930887668422949051881895212412718444016917144560705"
|
||||
"45675251775747156453237145919794089496168502517202869160"
|
||||
"78674893099371444940800865897607102159386345313384716752"
|
||||
"18590012064772045092956919481"),
|
||||
'q': long(1393384845225358996250882900535419012502712821577),
|
||||
'x': long(1220877188542930584999385210465204342686893855021),
|
||||
'y': long("14604423062661947579790240720337570315008549983452208015"
|
||||
"39426429789435409684914513123700756086453120500041882809"
|
||||
"10283610277194188071619191739512379408443695946763554493"
|
||||
"86398594314468629823767964702559709430618263927529765769"
|
||||
"10270265745700231533660131769648708944711006508965764877"
|
||||
"684264272082256183140297951")
|
||||
}
|
||||
|
||||
ECDatanistp256 = {
|
||||
'x': long('762825130203920963171185031449647317742997734817505505433829043'
|
||||
'45687059013883'),
|
||||
'y': long('815431978646028526322656647694416475343443758943143196810611371'
|
||||
'59310646683104'),
|
||||
'privateValue': long('3463874347721034170096400845565569825355565567882605'
|
||||
'9678074967909361042656500'),
|
||||
'curve': b'ecdsa-sha2-nistp256'
|
||||
}
|
||||
|
||||
ECDatanistp384 = {
|
||||
'privateValue': long('280814107134858470598753916394807521398239633534281633982576099083'
|
||||
'35787109896602102090002196616273211495718603965098'),
|
||||
'x': long('10036914308591746758780165503819213553101287571902957054148542'
|
||||
'504671046744460374996612408381962208627004841444205030'),
|
||||
'y': long('17337335659928075994560513699823544906448896792102247714689323'
|
||||
'575406618073069185107088229463828921069465902299522926'),
|
||||
'curve': b'ecdsa-sha2-nistp384'
|
||||
}
|
||||
|
||||
ECDatanistp521 = {
|
||||
'x': long('12944742826257420846659527752683763193401384271391513286022917'
|
||||
'29910013082920512632908350502247952686156279140016049549948975'
|
||||
'670668730618745449113644014505462'),
|
||||
'y': long('10784108810271976186737587749436295782985563640368689081052886'
|
||||
'16296815984553198866894145509329328086635278430266482551941240'
|
||||
'591605833440825557820439734509311'),
|
||||
'privateValue': long('662751235215460886290293902658128847495347691199214706697089140769'
|
||||
'672273950767961331442265530524063943548846724348048614239791498442'
|
||||
'5997823106818915698960565'),
|
||||
'curve': b'ecdsa-sha2-nistp521'
|
||||
}
|
||||
|
||||
privateECDSA_openssh521 = b"""-----BEGIN EC PRIVATE KEY-----
|
||||
MIHcAgEBBEIAjn0lSVF6QweS4bjOGP9RHwqxUiTastSE0MVuLtFvkxygZqQ712oZ
|
||||
ewMvqKkxthMQgxzSpGtRBcmkL7RqZ94+18qgBwYFK4EEACOhgYkDgYYABAFpX/6B
|
||||
mxxglwD+VpEvw0hcyxVzLxNnMGzxZGF7xmNj8nlF7M+TQctdlR2Xv/J+AgIeVGmB
|
||||
j2p84bkV9jBzrUNJEACsJjttZw8NbUrhxjkLT/3rMNtuwjE4vLja0P7DMTE0EV8X
|
||||
f09ETdku/z/1tOSSrSvRwmUcM9nQUJtHHAZlr5Q0fw==
|
||||
-----END EC PRIVATE KEY-----"""
|
||||
|
||||
# New format introduced in OpenSSH 6.5
|
||||
privateECDSA_openssh521_new = b"""-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBaV/+gZscYJcA/laRL8NIXMsVcy8T
|
||||
ZzBs8WRhe8ZjY/J5RezPk0HLXZUdl7/yfgICHlRpgY9qfOG5FfYwc61DSRAArCY7bWcPDW
|
||||
1K4cY5C0/96zDbbsIxOLy42tD+wzExNBFfF39PRE3ZLv8/9bTkkq0r0cJlHDPZ0FCbRxwG
|
||||
Za+UNH8AAAEAeRISlnkSEpYAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
|
||||
AAAIUEAWlf/oGbHGCXAP5WkS/DSFzLFXMvE2cwbPFkYXvGY2PyeUXsz5NBy12VHZe/8n4C
|
||||
Ah5UaYGPanzhuRX2MHOtQ0kQAKwmO21nDw1tSuHGOQtP/esw227CMTi8uNrQ/sMxMTQRXx
|
||||
d/T0RN2S7/P/W05JKtK9HCZRwz2dBQm0ccBmWvlDR/AAAAQgCOfSVJUXpDB5LhuM4Y/1Ef
|
||||
CrFSJNqy1ITQxW4u0W+THKBmpDvXahl7Ay+oqTG2ExCDHNKka1EFyaQvtGpn3j7XygAAAA
|
||||
ABAg==
|
||||
-----END OPENSSH PRIVATE KEY-----"""
|
||||
|
||||
publicECDSA_openssh521 = (
|
||||
b"ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACF"
|
||||
b"BAFpX/6BmxxglwD+VpEvw0hcyxVzLxNnMGzxZGF7xmNj8nlF7M+TQctdlR2Xv/J+AgIeVGmB"
|
||||
b"j2p84bkV9jBzrUNJEACsJjttZw8NbUrhxjkLT/3rMNtuwjE4vLja0P7DMTE0EV8Xf09ETdku"
|
||||
b"/z/1tOSSrSvRwmUcM9nQUJtHHAZlr5Q0fw== comment"
|
||||
)
|
||||
|
||||
privateECDSA_openssh384 = b"""-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDAtAi7I8j73WCX20qUM5hhHwHuFzYWYYILs2Sh8UZ+awNkARZ/Fu2LU
|
||||
LLl5RtOQpbWgBwYFK4EEACKhZANiAATU17sA9P5FRwSknKcFsjjsk0+E3CeXPYX0
|
||||
Tk/M0HK3PpWQWgrO8JdRHP9eFE9O/23P8BumwFt7F/AvPlCzVd35VfraFT0o4cCW
|
||||
G0RqpQ+np31aKmeJshkcYALEchnU+tQ=
|
||||
-----END EC PRIVATE KEY-----"""
|
||||
|
||||
# New format introduced in OpenSSH 6.5
|
||||
privateECDSA_openssh384_new = b"""-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTU17sA9P5FRwSknKcFsjjsk0+E3CeX
|
||||
PYX0Tk/M0HK3PpWQWgrO8JdRHP9eFE9O/23P8BumwFt7F/AvPlCzVd35VfraFT0o4cCWG0
|
||||
RqpQ+np31aKmeJshkcYALEchnU+tQAAADIiktpWIpLaVgAAAATZWNkc2Etc2hhMi1uaXN0
|
||||
cDM4NAAAAAhuaXN0cDM4NAAAAGEE1Ne7APT+RUcEpJynBbI47JNPhNwnlz2F9E5PzNBytz
|
||||
6VkFoKzvCXURz/XhRPTv9tz/AbpsBbexfwLz5Qs1Xd+VX62hU9KOHAlhtEaqUPp6d9Wipn
|
||||
ibIZHGACxHIZ1PrUAAAAMC0CLsjyPvdYJfbSpQzmGEfAe4XNhZhgguzZKHxRn5rA2QBFn8
|
||||
W7YtQsuXlG05CltQAAAAA=
|
||||
-----END OPENSSH PRIVATE KEY-----"""
|
||||
|
||||
publicECDSA_openssh384 = (
|
||||
b"ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABh"
|
||||
b"BNTXuwD0/kVHBKScpwWyOOyTT4TcJ5c9hfROT8zQcrc+lZBaCs7wl1Ec/14UT07/bc/wG6bA"
|
||||
b"W3sX8C8+ULNV3flV+toVPSjhwJYbRGqlD6enfVoqZ4myGRxgAsRyGdT61A== comment"
|
||||
)
|
||||
|
||||
publicECDSA_openssh = (
|
||||
b"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABB"
|
||||
b"BKimX1DZ7+Qj0SpfePMbo1pb6yGkAb5l7duC1l855yD7tEfQfqk7bc7v46We1hLMyz6ObUBY"
|
||||
b"gkN/34n42F4vpeA= comment"
|
||||
)
|
||||
|
||||
privateECDSA_openssh = b"""-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIEyU1YOT2JxxofwbJXIjGftdNcJK55aQdNrhIt2xYQz0oAoGCCqGSM49
|
||||
AwEHoUQDQgAEqKZfUNnv5CPRKl948xujWlvrIaQBvmXt24LWXznnIPu0R9B+qTtt
|
||||
zu/jpZ7WEszLPo5tQFiCQ3/fifjYXi+l4A==
|
||||
-----END EC PRIVATE KEY-----"""
|
||||
|
||||
# New format introduced in OpenSSH 6.5
|
||||
privateECDSA_openssh_new = b"""-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSopl9Q2e/kI9EqX3jzG6NaW+shpAG+
|
||||
Ze3bgtZfOecg+7RH0H6pO23O7+OlntYSzMs+jm1AWIJDf9+J+NheL6XgAAAAmCKU4hcilO
|
||||
IXAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKimX1DZ7+Qj0Spf
|
||||
ePMbo1pb6yGkAb5l7duC1l855yD7tEfQfqk7bc7v46We1hLMyz6ObUBYgkN/34n42F4vpe
|
||||
AAAAAgTJTVg5PYnHGh/BslciMZ+101wkrnlpB02uEi3bFhDPQAAAAA
|
||||
-----END OPENSSH PRIVATE KEY-----"""
|
||||
|
||||
publicRSA_openssh = (
|
||||
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDVaqx4I9bWG+wloVDEd2NQhEUBVUIUKirg"
|
||||
b"0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHjVyqgYwBGTJAkMgUyP"
|
||||
b"95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auIJNm/9NNN9b0b/h9qp"
|
||||
b"KSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY6RKXCpCnd1bqcPUWz"
|
||||
b"xiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvbw7KW4CC1ffdOgTtDc1"
|
||||
b"foNfICZgptyti8ZseZj3 comment"
|
||||
)
|
||||
|
||||
privateRSA_openssh = b'''-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEA1WqseCPW1hvsJaFQxHdjUIRFAVVCFCoq4NBg7tTpo61K+jkG
|
||||
XoRVdV8ANr9vqio/gyY3wWkuW/3w89J91pjNOkB41cqoGMARkyQJDIFMj/ec7RMW
|
||||
aqQE6Ul3w+RVZLN5aJ4sCOus6AQtIXcFp47vUzANpeW7PWriCTZv/TTTfW9G/4fa
|
||||
qSknqv+t9YXmPhq4eh1KserAWvcw3x/CpOTvP5FJlkDVGXctN8Ne7J2mOkSlwqQp
|
||||
3dW6nD1Fs8YsGGTVuj3fq3/NQqyn8RgLoFgVYgukKm5Dw+QEnzWjR45G7TOlZb28
|
||||
OyluAgtX33ToE7Q3NX6DXyAmYKbcrYvGbHmY9wIDAQABAoIBACFMCGaiKNW0+44P
|
||||
chuFCQC58k438BxXS+NRf54jp+Q6mFUb6ot6mB682Lqx+YkSGGCs6MwLTglaQGq6
|
||||
L5n4syRghLnOaZWa+eL8H1FNJxXbKyet77RprL59EOuGR3BztACHlRU7N/nnFOeA
|
||||
u2geG+bdu3NjuWfmsid/z88wm8KY/dkYNi82LvE9gXqf4QMtR9s0UWI53U/prKiL
|
||||
2dbzhMQXuXGdBghCeE27xSr0w1jNVSvtvjNfBOp75gQkY/It1z0bbNWcY0MvkoiN
|
||||
Pm7aGDfYDyVniR25RjReyc7Ei+2SWjMHD9+GCPmS6dvrOAg2yc3NCgFIWzk+esrG
|
||||
gKnc1DkCgYEA2XAG2OK81HiRUJTUwRuJOGxGZFpRoJoHPUiPA1HMaxKOfRqxZedx
|
||||
dTngMgV1jRhMr5OxSbFmX3hietEMyuZNQ7Oc9Gt95gyY3M8hYo7VLhLeBK7XJG6D
|
||||
MaIVokQ9IqliJiK5su1UCp0Ig6cHDf8ZGI7Yqx3aSJwxaBGhZm3j2B0CgYEA+0QX
|
||||
i6Q2vh43Haf2YWwExKrdeD4HjB4zAq4DFIeDeuWefQhnqPKqvxJwz3Kpp8cLHYjV
|
||||
IP2cY8pHMFVOi8TP9H8WpJISdKEJwsRunIwz76Xl9+ArrU9cEaoahDdb/Xrqw818
|
||||
sMjkH1Rjtcev3/QJp/zHJfxc6ZHXksWYHlbTsSMCgYBRr+mSn5QLSoRlPpSzO5IQ
|
||||
tXS4jMnvyQ4BMvovaBKhAyauz1FoFEwmmyikAjMIX+GncJgBNHleUo7Ezza8H0tV
|
||||
rOvBU4TH4WGoStSi/0ANgB8SqVDAKhh1lAwGmxZQqEvsQc177/dLyXUCaMSYuIaI
|
||||
GFpD5wIzlyJkk4MMRSp87QKBgGlmN8ZA3SHFBPOwuD5HlHx2/C3rPzk8lcNDAVHE
|
||||
Qpfz6Bakxu7s1EkQUDgE7jvN19DMzDJpkAegG1qf/jHNHjp+cR4ZlBpOTwzfX1LV
|
||||
0Rdu7NectlWd244hX7wkiLb8r6vw76QssNyfhrADEriL4t0PwO4jPUpQ/i+4KUZY
|
||||
v7YnAoGAZhb5IDTQVCW8YTGsgvvvnDUefkpVAmiVDQqTvh6/4UD6kKdUcDHpePzg
|
||||
Zrcid5rr3dXSMEbK4tdeQZvPtUg1Uaol3N7bNClIIdvWdPx+5S9T95wJcLnkoHam
|
||||
rXp0IjScTxfLP+Cq5V6lJ94/pX8Ppoj1FdZfNxeS4NYFSRA7kvY=
|
||||
-----END RSA PRIVATE KEY-----'''
|
||||
|
||||
# Some versions of OpenSSH generate these (slightly different keys): the PKCS#1
|
||||
# structure is wrapped in an extra ASN.1 SEQUENCE and there's an empty SEQUENCE
|
||||
# following it. It is not any standard key format and was probably a bug in
|
||||
# OpenSSH at some point.
|
||||
privateRSA_openssh_alternate = b"""-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEqTCCBKMCAQACggEBANVqrHgj1tYb7CWhUMR3Y1CERQFVQhQqKuDQYO7U6aOtSvo5Bl6EVXVf
|
||||
ADa/b6oqP4MmN8FpLlv98PPSfdaYzTpAeNXKqBjAEZMkCQyBTI/3nO0TFmqkBOlJd8PkVWSzeWie
|
||||
LAjrrOgELSF3BaeO71MwDaXluz1q4gk2b/00031vRv+H2qkpJ6r/rfWF5j4auHodSrHqwFr3MN8f
|
||||
wqTk7z+RSZZA1Rl3LTfDXuydpjpEpcKkKd3Vupw9RbPGLBhk1bo936t/zUKsp/EYC6BYFWILpCpu
|
||||
Q8PkBJ81o0eORu0zpWW9vDspbgILV9906BO0NzV+g18gJmCm3K2Lxmx5mPcCAwEAAQKCAQAhTAhm
|
||||
oijVtPuOD3IbhQkAufJON/AcV0vjUX+eI6fkOphVG+qLepgevNi6sfmJEhhgrOjMC04JWkBqui+Z
|
||||
+LMkYIS5zmmVmvni/B9RTScV2ysnre+0aay+fRDrhkdwc7QAh5UVOzf55xTngLtoHhvm3btzY7ln
|
||||
5rInf8/PMJvCmP3ZGDYvNi7xPYF6n+EDLUfbNFFiOd1P6ayoi9nW84TEF7lxnQYIQnhNu8Uq9MNY
|
||||
zVUr7b4zXwTqe+YEJGPyLdc9G2zVnGNDL5KIjT5u2hg32A8lZ4kduUY0XsnOxIvtklozBw/fhgj5
|
||||
kunb6zgINsnNzQoBSFs5PnrKxoCp3NQ5AoGBANlwBtjivNR4kVCU1MEbiThsRmRaUaCaBz1IjwNR
|
||||
zGsSjn0asWXncXU54DIFdY0YTK+TsUmxZl94YnrRDMrmTUOznPRrfeYMmNzPIWKO1S4S3gSu1yRu
|
||||
gzGiFaJEPSKpYiYiubLtVAqdCIOnBw3/GRiO2Ksd2kicMWgRoWZt49gdAoGBAPtEF4ukNr4eNx2n
|
||||
9mFsBMSq3Xg+B4weMwKuAxSHg3rlnn0IZ6jyqr8ScM9yqafHCx2I1SD9nGPKRzBVTovEz/R/FqSS
|
||||
EnShCcLEbpyMM++l5ffgK61PXBGqGoQ3W/166sPNfLDI5B9UY7XHr9/0Caf8xyX8XOmR15LFmB5W
|
||||
07EjAoGAUa/pkp+UC0qEZT6UszuSELV0uIzJ78kOATL6L2gSoQMmrs9RaBRMJpsopAIzCF/hp3CY
|
||||
ATR5XlKOxM82vB9LVazrwVOEx+FhqErUov9ADYAfEqlQwCoYdZQMBpsWUKhL7EHNe+/3S8l1AmjE
|
||||
mLiGiBhaQ+cCM5ciZJODDEUqfO0CgYBpZjfGQN0hxQTzsLg+R5R8dvwt6z85PJXDQwFRxEKX8+gW
|
||||
pMbu7NRJEFA4BO47zdfQzMwyaZAHoBtan/4xzR46fnEeGZQaTk8M319S1dEXbuzXnLZVnduOIV+8
|
||||
JIi2/K+r8O+kLLDcn4awAxK4i+LdD8DuIz1KUP4vuClGWL+2JwKBgQCFSxt6mxIQN54frV7a/saW
|
||||
/t81a7k04haXkiYJvb1wIAOnNb0tG6DSB0cr1N6oqAcHG7gEIKcnQTxsOTnpQc7nFx3RTFy8PdIm
|
||||
Jv5q1v1Icq5G+nvD0xlgRB2lE6eA9WMp1HpdBgcWXfaLPctkOuKEWk2MBi0tnRzrg0x4PXlUzjAA
|
||||
-----END RSA PRIVATE KEY-----"""
|
||||
|
||||
# New format introduced in OpenSSH 6.5
|
||||
privateRSA_openssh_new = b'''-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAQEA1WqseCPW1hvsJaFQxHdjUIRFAVVCFCoq4NBg7tTpo61K+jkGXoRV
|
||||
dV8ANr9vqio/gyY3wWkuW/3w89J91pjNOkB41cqoGMARkyQJDIFMj/ec7RMWaqQE6Ul3w+
|
||||
RVZLN5aJ4sCOus6AQtIXcFp47vUzANpeW7PWriCTZv/TTTfW9G/4faqSknqv+t9YXmPhq4
|
||||
eh1KserAWvcw3x/CpOTvP5FJlkDVGXctN8Ne7J2mOkSlwqQp3dW6nD1Fs8YsGGTVuj3fq3
|
||||
/NQqyn8RgLoFgVYgukKm5Dw+QEnzWjR45G7TOlZb28OyluAgtX33ToE7Q3NX6DXyAmYKbc
|
||||
rYvGbHmY9wAAA7gXkBoMF5AaDAAAAAdzc2gtcnNhAAABAQDVaqx4I9bWG+wloVDEd2NQhE
|
||||
UBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHjVyqgY
|
||||
wBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auIJNm
|
||||
/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY6
|
||||
RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvb
|
||||
w7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3AAAAAwEAAQAAAQAhTAhmoijVtPuOD3Ib
|
||||
hQkAufJON/AcV0vjUX+eI6fkOphVG+qLepgevNi6sfmJEhhgrOjMC04JWkBqui+Z+LMkYI
|
||||
S5zmmVmvni/B9RTScV2ysnre+0aay+fRDrhkdwc7QAh5UVOzf55xTngLtoHhvm3btzY7ln
|
||||
5rInf8/PMJvCmP3ZGDYvNi7xPYF6n+EDLUfbNFFiOd1P6ayoi9nW84TEF7lxnQYIQnhNu8
|
||||
Uq9MNYzVUr7b4zXwTqe+YEJGPyLdc9G2zVnGNDL5KIjT5u2hg32A8lZ4kduUY0XsnOxIvt
|
||||
klozBw/fhgj5kunb6zgINsnNzQoBSFs5PnrKxoCp3NQ5AAAAgQCFSxt6mxIQN54frV7a/s
|
||||
aW/t81a7k04haXkiYJvb1wIAOnNb0tG6DSB0cr1N6oqAcHG7gEIKcnQTxsOTnpQc7nFx3R
|
||||
TFy8PdImJv5q1v1Icq5G+nvD0xlgRB2lE6eA9WMp1HpdBgcWXfaLPctkOuKEWk2MBi0tnR
|
||||
zrg0x4PXlUzgAAAIEA2XAG2OK81HiRUJTUwRuJOGxGZFpRoJoHPUiPA1HMaxKOfRqxZedx
|
||||
dTngMgV1jRhMr5OxSbFmX3hietEMyuZNQ7Oc9Gt95gyY3M8hYo7VLhLeBK7XJG6DMaIVok
|
||||
Q9IqliJiK5su1UCp0Ig6cHDf8ZGI7Yqx3aSJwxaBGhZm3j2B0AAACBAPtEF4ukNr4eNx2n
|
||||
9mFsBMSq3Xg+B4weMwKuAxSHg3rlnn0IZ6jyqr8ScM9yqafHCx2I1SD9nGPKRzBVTovEz/
|
||||
R/FqSSEnShCcLEbpyMM++l5ffgK61PXBGqGoQ3W/166sPNfLDI5B9UY7XHr9/0Caf8xyX8
|
||||
XOmR15LFmB5W07EjAAAAAAEC
|
||||
-----END OPENSSH PRIVATE KEY-----'''
|
||||
|
||||
# Encrypted with the passphrase 'encrypted'
|
||||
privateRSA_openssh_encrypted = b"""-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,FFFFFFFFFFFFFFFF
|
||||
|
||||
p2A1YsHLXkpMVcsEqhh/nCYb5AqL0uMzfEIqc8hpZ/Ub8PtLsypilMkqzYTnZIGS
|
||||
ouyPjU/WgtR4VaDnutPWdgYaKdixSEmGhKghCtXFySZqCTJ4O8NCczsktYjUK3D4
|
||||
Jtl90zL6O81WBY6xP76PBQo9lrI/heAetATeyqutc18bwQIGU+gKk32qvfo15DfS
|
||||
VYiY0Ds4D7F7fd9pz+f5+UbFUCgU+tfDvBrqodYrUgmH7jKoW/CRDCHHyeEIZDbF
|
||||
mcMwdcKOyw1sRLaPdihRSVx3kOMvIotHKVTkIDMp+0RTNeXzQnp5U2qzsxzTcG/M
|
||||
UyJN38XXkuvq5VMj2zmmjHzx34w3NK3ZxpZcoaFUqUBlNp2C8hkCLrAa/DWobKqN
|
||||
5xA1ElrQvli9XXkT/RIuy4Gc10bbGEoJjuxNRibtSxxWd5Bd1E40ocOd4l1ebI8+
|
||||
w69XvMTnsmHvkBEADGF2zfRszKnMelg+W5NER1UDuNT03i+1cuhp+2AZg8z7niTO
|
||||
M17XP3ScGVxrQAEYgtxPrPeIpFJvOx2j5Yt78U9Y2WlaAG6DrubbYv2RsMIibhOG
|
||||
yk139vMdD8FwCey6yMkkhFAJwnBtC22MAWgjmC5c6AF3SRQSjjQXepPsJcLgpOjy
|
||||
YwjhnL8w56x9kVDUNPw9A9Cqgxo2sty34ATnKrh4h59PsP83LOL6OC5WjbASgZRd
|
||||
OIBD8RloQPISo+RUF7X0i4kdaHVNPlR0KyapR+3M5BwhQuvEO99IArDV2LNKGzfc
|
||||
W4ssugm8iyAJlmwmb2yRXIDHXabInWY7XCdGk8J2qPFbDTvnPbiagJBimjVjgpWw
|
||||
tV3sVlJYqmOqmCDP78J6he04l0vaHtiOWTDEmNCrK7oFMXIIp3XWjOZGPSOJFdPs
|
||||
6Go3YB+EGWfOQxqkFM28gcqmYfVPF2sa1FbZLz0ffO11Ma/rliZxZu7WdrAXe/tc
|
||||
BgIQ8etp2PwAK4jCwwVwjIO8FzqQGpS23Y9NY3rfi97ckgYXKESFtXPsMMA+drZd
|
||||
ThbXvccfh4EPmaqQXKf4WghHiVJ+/yuY1kUIDEl/O0jRZWT7STgBim/Aha1m6qRs
|
||||
zl1H7hkDbU4solb1GM5oPzbgGTzyBc+z0XxM9iFRM+fMzPB8+yYHTr4kPbVmKBjy
|
||||
SCovjQQVsHE4YeUGTq6k/NF5cVIRKTW/RlHvzxsky1Zj31MC736jrxGw4KG7VSLZ
|
||||
fP6F5jj+mXwS7m0v5to42JBZmRJdKUD88QaGE3ncyQ4yleW5bn9Lf9SuzQg1Dhao
|
||||
3rSA1RuexsHlIAHvGxx/17X+pyygl8DJbt6TBfbLQk9wc707DJTfh5M/bnk9wwIX
|
||||
l/Hsa1WtylAMW/2MzgiVy83MbYz4+Ss6GQ5W66okWji+NxrnrYEy6q+WgVQanp7X
|
||||
D+D7oKykqE1Cdvvulvtfl5fh8wlAs8mrUnKPBBUru348u++2lfacLkxRXyT1ooqY
|
||||
uSNE5nlwFt08N2Ou/bl7yq6QNRMYrRkn+UEfHWCNYDoGMHln2/i6Z1RapQzNarik
|
||||
tJf7radBz5nBwBjP08YAEACNSQvpsUgdqiuYjLwX7efFXQva2RzqaQ==
|
||||
-----END RSA PRIVATE KEY-----"""
|
||||
|
||||
# Encrypted with the passphrase 'encrypted', and using the new format
|
||||
# introduced in OpenSSH 6.5
|
||||
privateRSA_openssh_encrypted_new = b"""-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABD0f9WAof
|
||||
DTbmwztb8pdrSeAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDVaqx4I9bW
|
||||
G+wloVDEd2NQhEUBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n
|
||||
3WmM06QHjVyqgYwBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9T
|
||||
MA2l5bs9auIJNm/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQN
|
||||
UZdy03w17snaY6RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASf
|
||||
NaNHjkbtM6Vlvbw7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3AAADwPQaac8s1xX3af
|
||||
hQTQexj0vEAWDQsLYzDHN9G7W+UP5WHUu7igeu2GqAC/TOnjUXDP73I+EN3n7T3JFeDRfs
|
||||
U1Z6Zqb0NKHSRVYwDIdIi8qVohFv85g6+xQ01OpaoOzz+vI34OUvCRHQGTgR6L9fQShZyC
|
||||
McopYMYfbIse6KcqkfxX3KSdG1Pao6Njx/ShFRbgvmALpR/z0EaGCzHCDxpfUyAdnxm621
|
||||
Jzaf+LverWdN7sfrfMptaS9//9iJb70sL67K+YIB64qhDnA/w9UOQfXGQFL+AEtdM0BPv8
|
||||
thP1bs7T0yucBl+ZXdrDKVLZfaS3S/w85Jlgfu+a1DG73pOBOuag435iEJ9EnspjXiiydx
|
||||
GrfSRk2C+/c4fBDZVGFscK5bfQuUUZyU1qOagekxX7WLHFKk9xajnud+nrAN070SeNwlX8
|
||||
FZ2CI4KGlQfDvVUpKanYn8Kkj3fZ+YBGyx4M+19clF65FKSM0x1Rrh5tAmNT/SNDbSc28m
|
||||
ASxrBhztzxUFTrIn3tp+uqkJniFLmFsUtiAUmj8fNyE9blykU7dqq+CqpLA872nQ9bOHHA
|
||||
JsS1oBYmQ0n6AJz8WrYMdcepqWVld6Q8QSD1zdrY/sAWUovuBA1s4oIEXZhpXSS4ZJiMfh
|
||||
PVktKBwj5bmoG/mmwYLbo0JHntK8N3TGTzTGLq5TpSBBdVvWSWo7tnfEkrFObmhi1uJSrQ
|
||||
3zfPVP6BguboxBv+oxhaUBK8UOANe6ZwM4vfiu+QN+sZqWymHIfAktz7eWzwlToe4cKpdG
|
||||
Uv+e3/7Lo2dyMl3nke5HsSUrlsMGPREuGkBih8+o85ii6D+cuCiVtus3f5c78Cir80zLIr
|
||||
Z0wWvEAjciEvml00DWaA+JIaOrWwvXySaOzFGpCqC9SQjao379bvn9P3b7kVZsy6zBfHqm
|
||||
bNEJUOuhBZaY8Okz36chh1xqh4sz7m3nsZ3GYGcvM+3mvRY72QnqsQEG0Sp1XYIn2bHa29
|
||||
tqp7CG9X8J6dqMcPeoPRDWIX9gw7EPl/M0LP6xgewGJ9bgxwle6Mnr9kNITIswjAJqrLec
|
||||
zx7dfixjAPc42ADqrw/tEdFQcSqxigcfJNKO1LbDBjh+Hk/cSBou2PoxbIcl0qfQfbGcqI
|
||||
Dbpd695IEuiW9pYR22txNoIi+7cbMsuFHxQ/OqbrX/jCsprGNNJLAjgGsVEI1JnHWDH0db
|
||||
3UbqbOHAeY3ufoYXNY1utVOIACpW3r9wBw3FjRi04d70VcKr16OXvOAHGN2G++Y+kMya84
|
||||
Hl/Kt/gA==
|
||||
-----END OPENSSH PRIVATE KEY-----"""
|
||||
|
||||
# Encrypted with the passphrase 'testxp'. NB: this key was generated by
|
||||
# OpenSSH, so it doesn't use the same key data as the other keys here.
|
||||
privateRSA_openssh_encrypted_aes = b"""-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-128-CBC,0673309A6ACCAB4B77DEE1C1E536AC26
|
||||
|
||||
4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n
|
||||
T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H
|
||||
g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB
|
||||
sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5
|
||||
9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV
|
||||
gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW
|
||||
0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE
|
||||
vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS
|
||||
hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk
|
||||
2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf
|
||||
qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk
|
||||
4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY
|
||||
EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n
|
||||
8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0
|
||||
fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P
|
||||
V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+
|
||||
0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5
|
||||
xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI
|
||||
dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup
|
||||
VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk
|
||||
gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c
|
||||
8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw
|
||||
SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7
|
||||
CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE
|
||||
xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P
|
||||
-----END RSA PRIVATE KEY-----"""
|
||||
|
||||
publicRSA_lsh = (
|
||||
b'{KDEwOnB1YmxpYy1rZXkoMTQ6cnNhLXBrY3MxLXNoYTEoMTpuMjU3OgDVaqx4I9bWG+wloVD'
|
||||
b'Ed2NQhEUBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHj'
|
||||
b'VyqgYwBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auI'
|
||||
b'JNm/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY'
|
||||
b'6RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvbw'
|
||||
b'7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3KSgxOmUzOgEAASkpKQ==}'
|
||||
)
|
||||
|
||||
privateRSA_lsh = (
|
||||
b"(11:private-key(9:rsa-pkcs1(1:n257:\x00\xd5j\xacx#\xd6\xd6\x1b\xec%\xa1P"
|
||||
b"\xc4wcP\x84E\x01UB\x14**\xe0\xd0`\xee\xd4\xe9\xa3\xadJ\xfa9\x06^\x84Uu_"
|
||||
b"\x006\xbfo\xaa*?\x83&7\xc1i.[\xfd\xf0\xf3\xd2}\xd6\x98\xcd:@x\xd5\xca"
|
||||
b"\xa8\x18\xc0\x11\x93$\t\x0c\x81L\x8f\xf7\x9c\xed\x13\x16j\xa4\x04\xe9Iw"
|
||||
b"\xc3\xe4Ud\xb3yh\x9e,\x08\xeb\xac\xe8\x04-!w\x05\xa7\x8e\xefS0\r\xa5\xe5"
|
||||
b"\xbb=j\xe2\t6o\xfd4\xd3}oF\xff\x87\xda\xa9)'\xaa\xff\xad\xf5\x85\xe6>"
|
||||
b"\x1a\xb8z\x1dJ\xb1\xea\xc0Z\xf70\xdf\x1f\xc2\xa4\xe4\xef?\x91I\x96@\xd5"
|
||||
b"\x19w-7\xc3^\xec\x9d\xa6:D\xa5\xc2\xa4)\xdd\xd5\xba\x9c=E\xb3\xc6,\x18d"
|
||||
b"\xd5\xba=\xdf\xab\x7f\xcdB\xac\xa7\xf1\x18\x0b\xa0X\x15b\x0b\xa4*nC\xc3"
|
||||
b"\xe4\x04\x9f5\xa3G\x8eF\xed3\xa5e\xbd\xbc;)n\x02\x0bW\xdft\xe8\x13\xb475"
|
||||
b"~\x83_ &`\xa6\xdc\xad\x8b\xc6ly\x98\xf7)(1:e3:\x01\x00\x01)(1:d256:!L"
|
||||
b"\x08f\xa2(\xd5\xb4\xfb\x8e\x0fr\x1b\x85\t\x00\xb9\xf2N7\xf0\x1cWK\xe3Q"
|
||||
b"\x7f\x9e#\xa7\xe4:\x98U\x1b\xea\x8bz\x98\x1e\xbc\xd8\xba\xb1\xf9\x89\x12"
|
||||
b"\x18`\xac\xe8\xcc\x0bN\tZ@j\xba/\x99\xf8\xb3$`\x84\xb9\xcei\x95\x9a\xf9"
|
||||
b"\xe2\xfc\x1fQM'\x15\xdb+'\xad\xef\xb4i\xac\xbe}\x10\xeb\x86Gps\xb4\x00"
|
||||
b"\x87\x95\x15;7\xf9\xe7\x14\xe7\x80\xbbh\x1e\x1b\xe6\xdd\xbbsc\xb9g\xe6"
|
||||
b"\xb2'\x7f\xcf\xcf0\x9b\xc2\x98\xfd\xd9\x186/6.\xf1=\x81z\x9f\xe1\x03-G"
|
||||
b"\xdb4Qb9\xddO\xe9\xac\xa8\x8b\xd9\xd6\xf3\x84\xc4\x17\xb9q\x9d\x06\x08Bx"
|
||||
b"M\xbb\xc5*\xf4\xc3X\xcdU+\xed\xbe3_\x04\xea{\xe6\x04$c\xf2-\xd7=\x1bl"
|
||||
b"\xd5\x9ccC/\x92\x88\x8d>n\xda\x187\xd8\x0f%g\x89\x1d\xb9F4^\xc9\xce\xc4"
|
||||
b"\x8b\xed\x92Z3\x07\x0f\xdf\x86\x08\xf9\x92\xe9\xdb\xeb8\x086\xc9\xcd\xcd"
|
||||
b"\n\x01H[9>z\xca\xc6\x80\xa9\xdc\xd49)(1:p129:\x00\xfbD\x17\x8b\xa46\xbe"
|
||||
b"\x1e7\x1d\xa7\xf6al\x04\xc4\xaa\xddx>\x07\x8c\x1e3\x02\xae\x03\x14\x87"
|
||||
b"\x83z\xe5\x9e}\x08g\xa8\xf2\xaa\xbf\x12p\xcfr\xa9\xa7\xc7\x0b\x1d\x88"
|
||||
b"\xd5 \xfd\x9cc\xcaG0UN\x8b\xc4\xcf\xf4\x7f\x16\xa4\x92\x12t\xa1\t\xc2"
|
||||
b"\xc4n\x9c\x8c3\xef\xa5\xe5\xf7\xe0+\xadO\\\x11\xaa\x1a\x847[\xfdz\xea"
|
||||
b"\xc3\xcd|\xb0\xc8\xe4\x1fTc\xb5\xc7\xaf\xdf\xf4\t\xa7\xfc\xc7%\xfc\\\xe9"
|
||||
b"\x91\xd7\x92\xc5\x98\x1eV\xd3\xb1#)(1:q129:\x00\xd9p\x06\xd8\xe2\xbc\xd4"
|
||||
b"x\x91P\x94\xd4\xc1\x1b\x898lFdZQ\xa0\x9a\x07=H\x8f\x03Q\xcck\x12\x8e}"
|
||||
b"\x1a\xb1e\xe7qu9\xe02\x05u\x8d\x18L\xaf\x93\xb1I\xb1f_xbz\xd1\x0c\xca"
|
||||
b"\xe6MC\xb3\x9c\xf4k}\xe6\x0c\x98\xdc\xcf!b\x8e\xd5.\x12\xde\x04\xae\xd7$"
|
||||
b"n\x831\xa2\x15\xa2D=\"\xa9b&\"\xb9\xb2\xedT\n\x9d\x08\x83\xa7\x07\r\xff"
|
||||
b"\x19\x18\x8e\xd8\xab\x1d\xdaH\x9c1h\x11\xa1fm\xe3\xd8\x1d)(1:a128:if7"
|
||||
b"\xc6@\xdd!\xc5\x04\xf3\xb0\xb8>G\x94|v\xfc-\xeb?9<\x95\xc3C\x01Q\xc4B"
|
||||
b"\x97\xf3\xe8\x16\xa4\xc6\xee\xec\xd4I\x10P8\x04\xee;\xcd\xd7\xd0\xcc\xcc"
|
||||
b"2i\x90\x07\xa0\x1bZ\x9f\xfe1\xcd\x1e:~q\x1e\x19\x94\x1aNO\x0c\xdf_R\xd5"
|
||||
b"\xd1\x17n\xec\xd7\x9c\xb6U\x9d\xdb\x8e!_\xbc$\x88\xb6\xfc\xaf\xab\xf0"
|
||||
b"\xef\xa4,\xb0\xdc\x9f\x86\xb0\x03\x12\xb8\x8b\xe2\xdd\x0f\xc0\xee#=JP"
|
||||
b"\xfe/\xb8)FX\xbf\xb6')(1:b128:Q\xaf\xe9\x92\x9f\x94\x0bJ\x84e>\x94\xb3;"
|
||||
b"\x92\x10\xb5t\xb8\x8c\xc9\xef\xc9\x0e\x012\xfa/h\x12\xa1\x03&\xae\xcfQh"
|
||||
b"\x14L&\x9b(\xa4\x023\x08_\xe1\xa7p\x98\x014y^R\x8e\xc4\xcf6\xbc\x1fKU"
|
||||
b"\xac\xeb\xc1S\x84\xc7\xe1a\xa8J\xd4\xa2\xff@\r\x80\x1f\x12\xa9P\xc0*\x18"
|
||||
b"u\x94\x0c\x06\x9b\x16P\xa8K\xecA\xcd{\xef\xf7K\xc9u\x02h\xc4\x98\xb8\x86"
|
||||
b"\x88\x18ZC\xe7\x023\x97\"d\x93\x83\x0cE*|\xed)(1:c128:f\x16\xf9 4\xd0T%"
|
||||
b"\xbca1\xac\x82\xfb\xef\x9c5\x1e~JU\x02h\x95\r\n\x93\xbe\x1e\xbf\xe1@\xfa"
|
||||
b"\x90\xa7Tp1\xe9x\xfc\xe0f\xb7\"w\x9a\xeb\xdd\xd5\xd20F\xca\xe2\xd7^A\x9b"
|
||||
b"\xcf\xb5H5Q\xaa%\xdc\xde\xdb4)H!\xdb\xd6t\xfc~\xe5/S\xf7\x9c\tp\xb9\xe4"
|
||||
b"\xa0v\xa6\xadzt\"4\x9cO\x17\xcb?\xe0\xaa\xe5^\xa5\'\xde?\xa5\x7f\x0f\xa6"
|
||||
b"\x88\xf5\x15\xd6_7\x17\x92\xe0\xd6\x05I\x10;\x92\xf6)))"
|
||||
)
|
||||
|
||||
privateRSA_agentv3 = (
|
||||
b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x03\x01\x00\x01\x00\x00\x01\x00!L"
|
||||
b"\x08f\xa2(\xd5\xb4\xfb\x8e\x0fr\x1b\x85\t\x00\xb9\xf2N7\xf0\x1cWK\xe3Q"
|
||||
b"\x7f\x9e#\xa7\xe4:\x98U\x1b\xea\x8bz\x98\x1e\xbc\xd8\xba\xb1\xf9\x89\x12"
|
||||
b"\x18`\xac\xe8\xcc\x0bN\tZ@j\xba/\x99\xf8\xb3$`\x84\xb9\xcei\x95\x9a\xf9"
|
||||
b"\xe2\xfc\x1fQM'\x15\xdb+'\xad\xef\xb4i\xac\xbe}\x10\xeb\x86Gps\xb4\x00"
|
||||
b"\x87\x95\x15;7\xf9\xe7\x14\xe7\x80\xbbh\x1e\x1b\xe6\xdd\xbbsc\xb9g\xe6"
|
||||
b"\xb2'\x7f\xcf\xcf0\x9b\xc2\x98\xfd\xd9\x186/6.\xf1=\x81z\x9f\xe1\x03-G"
|
||||
b"\xdb4Qb9\xddO\xe9\xac\xa8\x8b\xd9\xd6\xf3\x84\xc4\x17\xb9q\x9d\x06\x08Bx"
|
||||
b"M\xbb\xc5*\xf4\xc3X\xcdU+\xed\xbe3_\x04\xea{\xe6\x04$c\xf2-\xd7=\x1bl"
|
||||
b"\xd5\x9ccC/\x92\x88\x8d>n\xda\x187\xd8\x0f%g\x89\x1d\xb9F4^\xc9\xce\xc4"
|
||||
b"\x8b\xed\x92Z3\x07\x0f\xdf\x86\x08\xf9\x92\xe9\xdb\xeb8\x086\xc9\xcd\xcd"
|
||||
b"\n\x01H[9>z\xca\xc6\x80\xa9\xdc\xd49\x00\x00\x01\x01\x00\xd5j\xacx#\xd6"
|
||||
b"\xd6\x1b\xec%\xa1P\xc4wcP\x84E\x01UB\x14**\xe0\xd0`\xee\xd4\xe9\xa3\xadJ"
|
||||
b"\xfa9\x06^\x84Uu_\x006\xbfo\xaa*?\x83&7\xc1i.[\xfd\xf0\xf3\xd2}\xd6\x98"
|
||||
b"\xcd:@x\xd5\xca\xa8\x18\xc0\x11\x93$\t\x0c\x81L\x8f\xf7\x9c\xed\x13\x16j"
|
||||
b"\xa4\x04\xe9Iw\xc3\xe4Ud\xb3yh\x9e,\x08\xeb\xac\xe8\x04-!w\x05\xa7\x8e"
|
||||
b"\xefS0\r\xa5\xe5\xbb=j\xe2\t6o\xfd4\xd3}oF\xff\x87\xda\xa9)'\xaa\xff\xad"
|
||||
b"\xf5\x85\xe6>\x1a\xb8z\x1dJ\xb1\xea\xc0Z\xf70\xdf\x1f\xc2\xa4\xe4\xef?"
|
||||
b"\x91I\x96@\xd5\x19w-7\xc3^\xec\x9d\xa6:D\xa5\xc2\xa4)\xdd\xd5\xba\x9c=E"
|
||||
b"\xb3\xc6,\x18d\xd5\xba=\xdf\xab\x7f\xcdB\xac\xa7\xf1\x18\x0b\xa0X\x15b"
|
||||
b"\x0b\xa4*nC\xc3\xe4\x04\x9f5\xa3G\x8eF\xed3\xa5e\xbd\xbc;)n\x02\x0bW\xdf"
|
||||
b"t\xe8\x13\xb475~\x83_ &`\xa6\xdc\xad\x8b\xc6ly\x98\xf7\x00\x00\x00\x81"
|
||||
b"\x00\x85K\x1bz\x9b\x12\x107\x9e\x1f\xad^\xda\xfe\xc6\x96\xfe\xdf5k\xb94"
|
||||
b"\xe2\x16\x97\x92&\t\xbd\xbdp \x03\xa75\xbd-\x1b\xa0\xd2\x07G+\xd4\xde"
|
||||
b"\xa8\xa8\x07\x07\x1b\xb8\x04 \xa7'A<l99\xe9A\xce\xe7\x17\x1d\xd1L\\\xbc="
|
||||
b"\xd2&&\xfej\xd6\xfdHr\xaeF\xfa{\xc3\xd3\x19`D\x1d\xa5\x13\xa7\x80\xf5c)"
|
||||
b"\xd4z]\x06\x07\x16]\xf6\x8b=\xcbd:\xe2\x84ZM\x8c\x06--\x9d\x1c\xeb\x83Lx"
|
||||
b"=yT\xce\x00\x00\x00\x81\x00\xd9p\x06\xd8\xe2\xbc\xd4x\x91P\x94\xd4\xc1"
|
||||
b"\x1b\x898lFdZQ\xa0\x9a\x07=H\x8f\x03Q\xcck\x12\x8e}\x1a\xb1e\xe7qu9\xe02"
|
||||
b"\x05u\x8d\x18L\xaf\x93\xb1I\xb1f_xbz\xd1\x0c\xca\xe6MC\xb3\x9c\xf4k}\xe6"
|
||||
b"\x0c\x98\xdc\xcf!b\x8e\xd5.\x12\xde\x04\xae\xd7$n\x831\xa2\x15\xa2D=\""
|
||||
b"\xa9b&\"\xb9\xb2\xedT\n\x9d\x08\x83\xa7\x07\r\xff\x19\x18\x8e\xd8\xab"
|
||||
b"\x1d\xdaH\x9c1h\x11\xa1fm\xe3\xd8\x1d\x00\x00\x00\x81\x00\xfbD\x17\x8b"
|
||||
b"\xa46\xbe\x1e7\x1d\xa7\xf6al\x04\xc4\xaa\xddx>\x07\x8c\x1e3\x02\xae\x03"
|
||||
b"\x14\x87\x83z\xe5\x9e}\x08g\xa8\xf2\xaa\xbf\x12p\xcfr\xa9\xa7\xc7\x0b"
|
||||
b"\x1d\x88\xd5 \xfd\x9cc\xcaG0UN\x8b\xc4\xcf\xf4\x7f\x16\xa4\x92\x12t\xa1"
|
||||
b"\t\xc2\xc4n\x9c\x8c3\xef\xa5\xe5\xf7\xe0+\xadO\\\x11\xaa\x1a\x847[\xfdz"
|
||||
b"\xea\xc3\xcd|\xb0\xc8\xe4\x1fTc\xb5\xc7\xaf\xdf\xf4\t\xa7\xfc\xc7%\xfc\\"
|
||||
b"\xe9\x91\xd7\x92\xc5\x98\x1eV\xd3\xb1#"
|
||||
)
|
||||
|
||||
publicDSA_openssh = b"""\
|
||||
ssh-dss AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9\
|
||||
LvFYmFFVMIuHFGlZpIL7sh3IMkqy+cssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+Gu48G\
|
||||
+yFuE8l0fVVUivos/MmYVJ66qT99htcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+Sx9a5AAAAFQD0\
|
||||
EYmTNaFJ8CS0+vFSF4nYcyEnSQAAAIEAkgLjxHJAE7qFWdTqf7EZngu7jAGmdB9k3YzMHe1ldMxEB\
|
||||
7zNw5aOnxjhoYLtiHeoEcOk2XOyvnE+VfhIWwWAdOiKRTEZlmizkvhGbq0DCe2EPMXirjqWACI5nD\
|
||||
ioQX1oEMonR8N3AEO5v9SfBqS2Q9R6OBr6lf04RvwpHZ0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQT\
|
||||
NEpklRZqeBGo1gotJggNmVaIQNIClGlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2G\
|
||||
gdgMQWC7S6WFIXePGGXqNQDdWxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8= \
|
||||
comment\
|
||||
"""
|
||||
|
||||
privateDSA_openssh = b"""\
|
||||
-----BEGIN DSA PRIVATE KEY-----
|
||||
MIIBvAIBAAKBgQCSkDrFREVQ0CKQDRx/iQAMpaPS7xWJhRVTCLhxRpWaSC+7IdyD
|
||||
JKsvnLLCDTP5Zxw935rAMi5VF2bbejw/S4GUWs7DzoKbbh/hruPBvshbhPJdH1VV
|
||||
Ir6LPzJmFSeuqk/fYbXGSmra01mZ6VVu4BQAaKsPoXti2dIJHlNPksfWuQIVAPQR
|
||||
iZM1oUnwJLT68VIXidhzISdJAoGBAJIC48RyQBO6hVnU6n+xGZ4Lu4wBpnQfZN2M
|
||||
zB3tZXTMRAe8zcOWjp8Y4aGC7Yh3qBHDpNlzsr5xPlX4SFsFgHToikUxGZZos5L4
|
||||
Rm6tAwnthDzF4q46lgAiOZw4qEF9aBDKJ0fDdwBDub/UnwaktkPUejga+pX9OEb8
|
||||
KR2dFBrvAoGAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQNICl
|
||||
GlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXeP
|
||||
GGXqNQDdWxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8CFQDV2gbL
|
||||
czUdxCus0pfEP1bddaXRLQ==
|
||||
-----END DSA PRIVATE KEY-----\
|
||||
"""
|
||||
|
||||
privateDSA_openssh_new = b"""\
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsgAAAAdzc2gtZH
|
||||
NzAAAAgQCSkDrFREVQ0CKQDRx/iQAMpaPS7xWJhRVTCLhxRpWaSC+7IdyDJKsvnLLCDTP5
|
||||
Zxw935rAMi5VF2bbejw/S4GUWs7DzoKbbh/hruPBvshbhPJdH1VVIr6LPzJmFSeuqk/fYb
|
||||
XGSmra01mZ6VVu4BQAaKsPoXti2dIJHlNPksfWuQAAABUA9BGJkzWhSfAktPrxUheJ2HMh
|
||||
J0kAAACBAJIC48RyQBO6hVnU6n+xGZ4Lu4wBpnQfZN2MzB3tZXTMRAe8zcOWjp8Y4aGC7Y
|
||||
h3qBHDpNlzsr5xPlX4SFsFgHToikUxGZZos5L4Rm6tAwnthDzF4q46lgAiOZw4qEF9aBDK
|
||||
J0fDdwBDub/UnwaktkPUejga+pX9OEb8KR2dFBrvAAAAgAIUacRjCFhMmhIfGJ44ms0EzR
|
||||
KZJUWangRqNYKLSYIDZlWiEDSApRpS8got+fXnxFLkHGfUl8TOfT/oXnHPxlPxh2pFuWFh
|
||||
OHT9hoHYDEFgu0ulhSF3jxhl6jUA3VsZV/LpoXp70KmtT5yqxUYQ6ycPGexo3R8X5bMQhJ
|
||||
lz6CzfAAAB2MVcBjzFXAY8AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9Lv
|
||||
FYmFFVMIuHFGlZpIL7sh3IMkqy+cssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+
|
||||
Gu48G+yFuE8l0fVVUivos/MmYVJ66qT99htcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+S
|
||||
x9a5AAAAFQD0EYmTNaFJ8CS0+vFSF4nYcyEnSQAAAIEAkgLjxHJAE7qFWdTqf7EZngu7jA
|
||||
GmdB9k3YzMHe1ldMxEB7zNw5aOnxjhoYLtiHeoEcOk2XOyvnE+VfhIWwWAdOiKRTEZlmiz
|
||||
kvhGbq0DCe2EPMXirjqWACI5nDioQX1oEMonR8N3AEO5v9SfBqS2Q9R6OBr6lf04RvwpHZ
|
||||
0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQNIClGlLyCi35
|
||||
9efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXePGGXqNQDdWxlX8u
|
||||
mhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8AAAAVANXaBstzNR3EK6zSl8Q/Vt11
|
||||
pdEtAAAAAAE=
|
||||
-----END OPENSSH PRIVATE KEY-----\
|
||||
"""
|
||||
|
||||
publicDSA_lsh = decodebytes(b"""\
|
||||
e0tERXdPbkIxWW14cFl5MXJaWGtvTXpwa2MyRW9NVHB3TVRJNU9nQ1NrRHJGUkVWUTBDS1FEUngv
|
||||
aVFBTXBhUFM3eFdKaFJWVENMaHhScFdhU0MrN0lkeURKS3N2bkxMQ0RUUDVaeHc5MzVyQU1pNVZG
|
||||
MmJiZWp3L1M0R1VXczdEem9LYmJoL2hydVBCdnNoYmhQSmRIMVZWSXI2TFB6Sm1GU2V1cWsvZlli
|
||||
WEdTbXJhMDFtWjZWVnU0QlFBYUtzUG9YdGkyZElKSGxOUGtzZld1U2tvTVRweE1qRTZBUFFSaVpN
|
||||
MW9VbndKTFQ2OFZJWGlkaHpJU2RKS1NneE9tY3hNams2QUpJQzQ4UnlRQk82aFZuVTZuK3hHWjRM
|
||||
dTR3QnBuUWZaTjJNekIzdFpYVE1SQWU4emNPV2pwOFk0YUdDN1loM3FCSERwTmx6c3I1eFBsWDRT
|
||||
RnNGZ0hUb2lrVXhHWlpvczVMNFJtNnRBd250aER6RjRxNDZsZ0FpT1p3NHFFRjlhQkRLSjBmRGR3
|
||||
QkR1Yi9Vbndha3RrUFVlamdhK3BYOU9FYjhLUjJkRkJydktTZ3hPbmt4TWpnNkFoUnB4R01JV0V5
|
||||
YUVoOFluamlhelFUTkVwa2xSWnFlQkdvMWdvdEpnZ05tVmFJUU5JQ2xHbEx5Q2kzNTllZkVVdVFj
|
||||
WjlTWHhNNTlQK2hlY2MvR1UvR0hha1c1WVdFNGRQMkdnZGdNUVdDN1M2V0ZJWGVQR0dYcU5RRGRX
|
||||
eGxYOHVtaGVudlFxYTFQbktyRlJoRHJKdzhaN0dqZEh4ZmxzeENFbVhQb0xOOHBLU2s9fQ==
|
||||
""")
|
||||
|
||||
privateDSA_lsh = decodebytes(b"""\
|
||||
KDExOnByaXZhdGUta2V5KDM6ZHNhKDE6cDEyOToAkpA6xURFUNAikA0cf4kADKWj0u8ViYUVUwi4
|
||||
cUaVmkgvuyHcgySrL5yywg0z+WccPd+awDIuVRdm23o8P0uBlFrOw86Cm24f4a7jwb7IW4TyXR9V
|
||||
VSK+iz8yZhUnrqpP32G1xkpq2tNZmelVbuAUAGirD6F7YtnSCR5TT5LH1rkpKDE6cTIxOgD0EYmT
|
||||
NaFJ8CS0+vFSF4nYcyEnSSkoMTpnMTI5OgCSAuPEckATuoVZ1Op/sRmeC7uMAaZ0H2TdjMwd7WV0
|
||||
zEQHvM3Dlo6fGOGhgu2Id6gRw6TZc7K+cT5V+EhbBYB06IpFMRmWaLOS+EZurQMJ7YQ8xeKuOpYA
|
||||
IjmcOKhBfWgQyidHw3cAQ7m/1J8GpLZD1Ho4GvqV/ThG/CkdnRQa7ykoMTp5MTI4OgIUacRjCFhM
|
||||
mhIfGJ44ms0EzRKZJUWangRqNYKLSYIDZlWiEDSApRpS8got+fXnxFLkHGfUl8TOfT/oXnHPxlPx
|
||||
h2pFuWFhOHT9hoHYDEFgu0ulhSF3jxhl6jUA3VsZV/LpoXp70KmtT5yqxUYQ6ycPGexo3R8X5bMQ
|
||||
hJlz6CzfKSgxOngyMToA1doGy3M1HcQrrNKXxD9W3XWl0S0pKSk=
|
||||
""")
|
||||
|
||||
privateDSA_agentv3 = decodebytes(b"""\
|
||||
AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9LvFYmFFVMIuHFGlZpIL7sh3IMkqy+c
|
||||
ssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+Gu48G+yFuE8l0fVVUivos/MmYVJ66qT99h
|
||||
tcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+Sx9a5AAAAFQD0EYmTNaFJ8CS0+vFSF4nYcyEnSQAA
|
||||
AIEAkgLjxHJAE7qFWdTqf7EZngu7jAGmdB9k3YzMHe1ldMxEB7zNw5aOnxjhoYLtiHeoEcOk2XOy
|
||||
vnE+VfhIWwWAdOiKRTEZlmizkvhGbq0DCe2EPMXirjqWACI5nDioQX1oEMonR8N3AEO5v9SfBqS2
|
||||
Q9R6OBr6lf04RvwpHZ0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQ
|
||||
NIClGlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXePGGXqNQDd
|
||||
WxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8AAAAVANXaBstzNR3EK6zSl8Q/Vt11
|
||||
pdEt
|
||||
""")
|
||||
|
||||
__all__ = ['DSAData', 'RSAData', 'privateDSA_agentv3', 'privateDSA_lsh',
|
||||
'privateDSA_openssh', 'privateRSA_agentv3', 'privateRSA_lsh',
|
||||
'privateRSA_openssh', 'publicDSA_lsh', 'publicDSA_openssh',
|
||||
'publicRSA_lsh', 'publicRSA_openssh', 'privateRSA_openssh_alternate']
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Loopback helper used in test_ssh and test_recvline
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.protocols import loopback
|
||||
class LoopbackRelay(loopback.LoopbackRelay):
|
||||
clearCall = None
|
||||
|
||||
def logPrefix(self):
|
||||
return "LoopbackRelay(%r)" % (self.target.__class__.__name__,)
|
||||
|
||||
|
||||
def write(self, data):
|
||||
loopback.LoopbackRelay.write(self, data)
|
||||
if self.clearCall is not None:
|
||||
self.clearCall.cancel()
|
||||
|
||||
from twisted.internet import reactor
|
||||
self.clearCall = reactor.callLater(0, self._clearBuffer)
|
||||
|
||||
|
||||
def _clearBuffer(self):
|
||||
self.clearCall = None
|
||||
loopback.LoopbackRelay.clearBuffer(self)
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{SSHTransportAddrress} in ssh/address.py
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet.address import IPv4Address
|
||||
from twisted.internet.test.test_address import AddressTestCaseMixin
|
||||
|
||||
from twisted.conch.ssh.address import SSHTransportAddress
|
||||
|
||||
|
||||
|
||||
class SSHTransportAddressTests(unittest.TestCase, AddressTestCaseMixin):
|
||||
"""
|
||||
L{twisted.conch.ssh.address.SSHTransportAddress} is what Conch transports
|
||||
use to represent the other side of the SSH connection. This tests the
|
||||
basic functionality of that class (string representation, comparison, &c).
|
||||
"""
|
||||
|
||||
|
||||
def _stringRepresentation(self, stringFunction):
|
||||
"""
|
||||
The string representation of C{SSHTransportAddress} should be
|
||||
"SSHTransportAddress(<stringFunction on address>)".
|
||||
"""
|
||||
addr = self.buildAddress()
|
||||
stringValue = stringFunction(addr)
|
||||
addressValue = stringFunction(addr.address)
|
||||
self.assertEqual(stringValue,
|
||||
"SSHTransportAddress(%s)" % addressValue)
|
||||
|
||||
|
||||
def buildAddress(self):
|
||||
"""
|
||||
Create an arbitrary new C{SSHTransportAddress}. A new instance is
|
||||
created for each call, but always for the same address.
|
||||
"""
|
||||
return SSHTransportAddress(IPv4Address("TCP", "127.0.0.1", 22))
|
||||
|
||||
|
||||
def buildDifferentAddress(self):
|
||||
"""
|
||||
Like C{buildAddress}, but with a different fixed address.
|
||||
"""
|
||||
return SSHTransportAddress(IPv4Address("TCP", "127.0.0.2", 22))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,567 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.scripts.ckeygen}.
|
||||
"""
|
||||
|
||||
import getpass
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
from twisted.python.compat import unicode, _PY3
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
if requireModule('cryptography') and requireModule('pyasn1'):
|
||||
from twisted.conch.ssh.keys import (Key, BadKeyError,
|
||||
BadFingerPrintFormat, FingerprintFormats)
|
||||
from twisted.conch.scripts.ckeygen import (
|
||||
changePassPhrase, displayPublicKey, printFingerprint,
|
||||
_saveKey, enumrepresentation)
|
||||
else:
|
||||
skip = "cryptography and pyasn1 required for twisted.conch.scripts.ckeygen"
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.conch.test.keydata import (
|
||||
publicRSA_openssh, privateRSA_openssh, privateRSA_openssh_encrypted, privateECDSA_openssh)
|
||||
|
||||
|
||||
|
||||
def makeGetpass(*passphrases):
|
||||
"""
|
||||
Return a callable to patch C{getpass.getpass}. Yields a passphrase each
|
||||
time called. Use case is to provide an old, then new passphrase(s) as if
|
||||
requested interactively.
|
||||
|
||||
@param passphrases: The list of passphrases returned, one per each call.
|
||||
|
||||
@return: A callable to patch C{getpass.getpass}.
|
||||
"""
|
||||
passphrases = iter(passphrases)
|
||||
|
||||
def fakeGetpass(_):
|
||||
return next(passphrases)
|
||||
|
||||
return fakeGetpass
|
||||
|
||||
|
||||
|
||||
class KeyGenTests(TestCase):
|
||||
"""
|
||||
Tests for various functions used to implement the I{ckeygen} script.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Patch C{sys.stdout} so tests can make assertions about what's printed.
|
||||
"""
|
||||
if _PY3:
|
||||
self.stdout = StringIO()
|
||||
else:
|
||||
self.stdout = BytesIO()
|
||||
self.patch(sys, 'stdout', self.stdout)
|
||||
|
||||
|
||||
|
||||
def _testrun(self, keyType, keySize=None):
|
||||
filename = self.mktemp()
|
||||
if keySize is None:
|
||||
subprocess.call(['ckeygen', '-t', keyType, '-f', filename, '--no-passphrase'])
|
||||
else:
|
||||
subprocess.call(['ckeygen', '-t', keyType, '-f', filename, '--no-passphrase',
|
||||
'-b', keySize])
|
||||
privKey = Key.fromFile(filename)
|
||||
pubKey = Key.fromFile(filename + '.pub')
|
||||
if keyType == 'ecdsa':
|
||||
self.assertEqual(privKey.type(), 'EC')
|
||||
else:
|
||||
self.assertEqual(privKey.type(), keyType.upper())
|
||||
self.assertTrue(pubKey.isPublic())
|
||||
|
||||
|
||||
def test_keygeneration(self):
|
||||
self._testrun('ecdsa', '384')
|
||||
self._testrun('ecdsa')
|
||||
self._testrun('dsa', '2048')
|
||||
self._testrun('dsa')
|
||||
self._testrun('rsa', '2048')
|
||||
self._testrun('rsa')
|
||||
|
||||
|
||||
|
||||
def test_runBadKeytype(self):
|
||||
filename = self.mktemp()
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
subprocess.check_call(['ckeygen', '-t', 'foo', '-f', filename])
|
||||
|
||||
|
||||
|
||||
def test_enumrepresentation(self):
|
||||
"""
|
||||
L{enumrepresentation} takes a dictionary as input and returns a
|
||||
dictionary with its attributes changed to enum representation.
|
||||
"""
|
||||
options = enumrepresentation({'format': 'md5-hex'})
|
||||
self.assertIs(options['format'],
|
||||
FingerprintFormats.MD5_HEX)
|
||||
|
||||
|
||||
def test_enumrepresentationsha256(self):
|
||||
"""
|
||||
Test for format L{FingerprintFormats.SHA256-BASE64}.
|
||||
"""
|
||||
options = enumrepresentation({'format': 'sha256-base64'})
|
||||
self.assertIs(options['format'],
|
||||
FingerprintFormats.SHA256_BASE64)
|
||||
|
||||
|
||||
|
||||
def test_enumrepresentationBadFormat(self):
|
||||
"""
|
||||
Test for unsupported fingerprint format
|
||||
"""
|
||||
with self.assertRaises(BadFingerPrintFormat) as em:
|
||||
enumrepresentation({'format': 'sha-base64'})
|
||||
self.assertEqual('Unsupported fingerprint format: sha-base64',
|
||||
em.exception.args[0])
|
||||
|
||||
|
||||
|
||||
def test_printFingerprint(self):
|
||||
"""
|
||||
L{printFingerprint} writes a line to standard out giving the number of
|
||||
bits of the key, its fingerprint, and the basename of the file from it
|
||||
was read.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(publicRSA_openssh)
|
||||
printFingerprint({'filename': filename,
|
||||
'format': 'md5-hex'})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
'2048 85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da temp\n')
|
||||
|
||||
|
||||
def test_printFingerprintsha256(self):
|
||||
"""
|
||||
L{printFigerprint} will print key fingerprint in
|
||||
L{FingerprintFormats.SHA256-BASE64} format if explicitly specified.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(publicRSA_openssh)
|
||||
printFingerprint({'filename': filename,
|
||||
'format': 'sha256-base64'})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
'2048 FBTCOoknq0mHy+kpfnY9tDdcAJuWtCpuQMaV3EsvbUI= temp\n')
|
||||
|
||||
|
||||
def test_printFingerprintBadFingerPrintFormat(self):
|
||||
"""
|
||||
L{printFigerprint} raises C{keys.BadFingerprintFormat} when unsupported
|
||||
formats are requested.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(publicRSA_openssh)
|
||||
with self.assertRaises(BadFingerPrintFormat) as em:
|
||||
printFingerprint({'filename': filename, 'format':'sha-base64'})
|
||||
self.assertEqual('Unsupported fingerprint format: sha-base64',
|
||||
em.exception.args[0])
|
||||
|
||||
|
||||
|
||||
def test_saveKey(self):
|
||||
"""
|
||||
L{_saveKey} writes the private and public parts of a key to two
|
||||
different files and writes a report of this to standard out.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child('id_rsa').path
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
_saveKey(key, {'filename': filename, 'pass': 'passphrase',
|
||||
'format': 'md5-hex'})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"Your identification has been saved in %s\n"
|
||||
"Your public key has been saved in %s.pub\n"
|
||||
"The key fingerprint in <FingerprintFormats=MD5_HEX> is:\n"
|
||||
"85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da\n" % (
|
||||
filename,
|
||||
filename))
|
||||
self.assertEqual(
|
||||
key.fromString(
|
||||
base.child('id_rsa').getContent(), None, 'passphrase'),
|
||||
key)
|
||||
self.assertEqual(
|
||||
Key.fromString(base.child('id_rsa.pub').getContent()),
|
||||
key.public())
|
||||
|
||||
|
||||
def test_saveKeyECDSA(self):
|
||||
"""
|
||||
L{_saveKey} writes the private and public parts of a key to two
|
||||
different files and writes a report of this to standard out.
|
||||
Test with ECDSA key.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child('id_ecdsa').path
|
||||
key = Key.fromString(privateECDSA_openssh)
|
||||
_saveKey(key, {'filename': filename, 'pass': 'passphrase',
|
||||
'format': 'md5-hex'})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"Your identification has been saved in %s\n"
|
||||
"Your public key has been saved in %s.pub\n"
|
||||
"The key fingerprint in <FingerprintFormats=MD5_HEX> is:\n"
|
||||
"1e:ab:83:a6:f2:04:22:99:7c:64:14:d2:ab:fa:f5:16\n" % (
|
||||
filename,
|
||||
filename))
|
||||
self.assertEqual(
|
||||
key.fromString(
|
||||
base.child('id_ecdsa').getContent(), None, 'passphrase'),
|
||||
key)
|
||||
self.assertEqual(
|
||||
Key.fromString(base.child('id_ecdsa.pub').getContent()),
|
||||
key.public())
|
||||
|
||||
|
||||
def test_saveKeysha256(self):
|
||||
"""
|
||||
L{_saveKey} will generate key fingerprint in
|
||||
L{FingerprintFormats.SHA256-BASE64} format if explicitly specified.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child('id_rsa').path
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
_saveKey(key, {'filename': filename, 'pass': 'passphrase',
|
||||
'format': 'sha256-base64'})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"Your identification has been saved in %s\n"
|
||||
"Your public key has been saved in %s.pub\n"
|
||||
"The key fingerprint in <FingerprintFormats=SHA256_BASE64> is:\n"
|
||||
"FBTCOoknq0mHy+kpfnY9tDdcAJuWtCpuQMaV3EsvbUI=\n" % (
|
||||
filename,
|
||||
filename))
|
||||
self.assertEqual(
|
||||
key.fromString(
|
||||
base.child('id_rsa').getContent(), None, 'passphrase'),
|
||||
key)
|
||||
self.assertEqual(
|
||||
Key.fromString(base.child('id_rsa.pub').getContent()),
|
||||
key.public())
|
||||
|
||||
|
||||
def test_saveKeyBadFingerPrintformat(self):
|
||||
"""
|
||||
L{_saveKey} raises C{keys.BadFingerprintFormat} when unsupported
|
||||
formats are requested.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child('id_rsa').path
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
with self.assertRaises(BadFingerPrintFormat) as em:
|
||||
_saveKey(key, {'filename': filename, 'pass': 'passphrase',
|
||||
'format': 'sha-base64'})
|
||||
self.assertEqual('Unsupported fingerprint format: sha-base64',
|
||||
em.exception.args[0])
|
||||
|
||||
|
||||
def test_saveKeyEmptyPassphrase(self):
|
||||
"""
|
||||
L{_saveKey} will choose an empty string for the passphrase if
|
||||
no-passphrase is C{True}.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child('id_rsa').path
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
_saveKey(key, {'filename': filename, 'no-passphrase': True,
|
||||
'format': 'md5-hex'})
|
||||
self.assertEqual(
|
||||
key.fromString(
|
||||
base.child('id_rsa').getContent(), None, b''),
|
||||
key)
|
||||
|
||||
|
||||
def test_saveKeyECDSAEmptyPassphrase(self):
|
||||
"""
|
||||
L{_saveKey} will choose an empty string for the passphrase if
|
||||
no-passphrase is C{True}.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child('id_ecdsa').path
|
||||
key = Key.fromString(privateECDSA_openssh)
|
||||
_saveKey(key, {'filename': filename, 'no-passphrase': True,
|
||||
'format': 'md5-hex'})
|
||||
self.assertEqual(
|
||||
key.fromString(
|
||||
base.child('id_ecdsa').getContent(), None),
|
||||
key)
|
||||
|
||||
|
||||
|
||||
def test_saveKeyNoFilename(self):
|
||||
"""
|
||||
When no path is specified, it will ask for the path used to store the
|
||||
key.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
keyPath = base.child('custom_key').path
|
||||
|
||||
import twisted.conch.scripts.ckeygen
|
||||
self.patch(twisted.conch.scripts.ckeygen, 'raw_input', lambda _: keyPath)
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
_saveKey(key, {'filename': None, 'no-passphrase': True,
|
||||
'format': 'md5-hex'})
|
||||
|
||||
persistedKeyContent = base.child('custom_key').getContent()
|
||||
persistedKey = key.fromString(persistedKeyContent, None, b'')
|
||||
self.assertEqual(key, persistedKey)
|
||||
|
||||
|
||||
def test_displayPublicKey(self):
|
||||
"""
|
||||
L{displayPublicKey} prints out the public key associated with a given
|
||||
private key.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
pubKey = Key.fromString(publicRSA_openssh)
|
||||
FilePath(filename).setContent(privateRSA_openssh)
|
||||
displayPublicKey({'filename': filename})
|
||||
displayed = self.stdout.getvalue().strip('\n')
|
||||
if isinstance(displayed, unicode):
|
||||
displayed = displayed.encode("ascii")
|
||||
self.assertEqual(
|
||||
displayed,
|
||||
pubKey.toString('openssh'))
|
||||
|
||||
|
||||
def test_displayPublicKeyEncrypted(self):
|
||||
"""
|
||||
L{displayPublicKey} prints out the public key associated with a given
|
||||
private key using the given passphrase when it's encrypted.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
pubKey = Key.fromString(publicRSA_openssh)
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
displayPublicKey({'filename': filename, 'pass': 'encrypted'})
|
||||
displayed = self.stdout.getvalue().strip('\n')
|
||||
if isinstance(displayed, unicode):
|
||||
displayed = displayed.encode("ascii")
|
||||
self.assertEqual(
|
||||
displayed,
|
||||
pubKey.toString('openssh'))
|
||||
|
||||
|
||||
def test_displayPublicKeyEncryptedPassphrasePrompt(self):
|
||||
"""
|
||||
L{displayPublicKey} prints out the public key associated with a given
|
||||
private key, asking for the passphrase when it's encrypted.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
pubKey = Key.fromString(publicRSA_openssh)
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
self.patch(getpass, 'getpass', lambda x: 'encrypted')
|
||||
displayPublicKey({'filename': filename})
|
||||
displayed = self.stdout.getvalue().strip('\n')
|
||||
if isinstance(displayed, unicode):
|
||||
displayed = displayed.encode("ascii")
|
||||
self.assertEqual(
|
||||
displayed,
|
||||
pubKey.toString('openssh'))
|
||||
|
||||
|
||||
def test_displayPublicKeyWrongPassphrase(self):
|
||||
"""
|
||||
L{displayPublicKey} fails with a L{BadKeyError} when trying to decrypt
|
||||
an encrypted key with the wrong password.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
self.assertRaises(
|
||||
BadKeyError, displayPublicKey,
|
||||
{'filename': filename, 'pass': 'wrong'})
|
||||
|
||||
|
||||
def test_changePassphrase(self):
|
||||
"""
|
||||
L{changePassPhrase} allows a user to change the passphrase of a
|
||||
private key interactively.
|
||||
"""
|
||||
oldNewConfirm = makeGetpass('encrypted', 'newpass', 'newpass')
|
||||
self.patch(getpass, 'getpass', oldNewConfirm)
|
||||
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
|
||||
changePassPhrase({'filename': filename})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue().strip('\n'),
|
||||
'Your identification has been saved with the new passphrase.')
|
||||
self.assertNotEqual(privateRSA_openssh_encrypted,
|
||||
FilePath(filename).getContent())
|
||||
|
||||
|
||||
def test_changePassphraseWithOld(self):
|
||||
"""
|
||||
L{changePassPhrase} allows a user to change the passphrase of a
|
||||
private key, providing the old passphrase and prompting for new one.
|
||||
"""
|
||||
newConfirm = makeGetpass('newpass', 'newpass')
|
||||
self.patch(getpass, 'getpass', newConfirm)
|
||||
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
|
||||
changePassPhrase({'filename': filename, 'pass': 'encrypted'})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue().strip('\n'),
|
||||
'Your identification has been saved with the new passphrase.')
|
||||
self.assertNotEqual(privateRSA_openssh_encrypted,
|
||||
FilePath(filename).getContent())
|
||||
|
||||
|
||||
def test_changePassphraseWithBoth(self):
|
||||
"""
|
||||
L{changePassPhrase} allows a user to change the passphrase of a private
|
||||
key by providing both old and new passphrases without prompting.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
|
||||
changePassPhrase(
|
||||
{'filename': filename, 'pass': 'encrypted',
|
||||
'newpass': 'newencrypt'})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue().strip('\n'),
|
||||
'Your identification has been saved with the new passphrase.')
|
||||
self.assertNotEqual(privateRSA_openssh_encrypted,
|
||||
FilePath(filename).getContent())
|
||||
|
||||
|
||||
def test_changePassphraseWrongPassphrase(self):
|
||||
"""
|
||||
L{changePassPhrase} exits if passed an invalid old passphrase when
|
||||
trying to change the passphrase of a private key.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
error = self.assertRaises(
|
||||
SystemExit, changePassPhrase,
|
||||
{'filename': filename, 'pass': 'wrong'})
|
||||
self.assertEqual('Could not change passphrase: old passphrase error',
|
||||
str(error))
|
||||
self.assertEqual(privateRSA_openssh_encrypted,
|
||||
FilePath(filename).getContent())
|
||||
|
||||
|
||||
def test_changePassphraseEmptyGetPass(self):
|
||||
"""
|
||||
L{changePassPhrase} exits if no passphrase is specified for the
|
||||
C{getpass} call and the key is encrypted.
|
||||
"""
|
||||
self.patch(getpass, 'getpass', makeGetpass(''))
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
error = self.assertRaises(
|
||||
SystemExit, changePassPhrase, {'filename': filename})
|
||||
self.assertEqual(
|
||||
'Could not change passphrase: Passphrase must be provided '
|
||||
'for an encrypted key',
|
||||
str(error))
|
||||
self.assertEqual(privateRSA_openssh_encrypted,
|
||||
FilePath(filename).getContent())
|
||||
|
||||
|
||||
def test_changePassphraseBadKey(self):
|
||||
"""
|
||||
L{changePassPhrase} exits if the file specified points to an invalid
|
||||
key.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(b'foobar')
|
||||
error = self.assertRaises(
|
||||
SystemExit, changePassPhrase, {'filename': filename})
|
||||
|
||||
if _PY3:
|
||||
expected = "Could not change passphrase: cannot guess the type of b'foobar'"
|
||||
else:
|
||||
expected = "Could not change passphrase: cannot guess the type of 'foobar'"
|
||||
self.assertEqual(expected, str(error))
|
||||
self.assertEqual(b'foobar', FilePath(filename).getContent())
|
||||
|
||||
|
||||
def test_changePassphraseCreateError(self):
|
||||
"""
|
||||
L{changePassPhrase} doesn't modify the key file if an unexpected error
|
||||
happens when trying to create the key with the new passphrase.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh)
|
||||
|
||||
def toString(*args, **kwargs):
|
||||
raise RuntimeError('oops')
|
||||
|
||||
self.patch(Key, 'toString', toString)
|
||||
|
||||
error = self.assertRaises(
|
||||
SystemExit, changePassPhrase,
|
||||
{'filename': filename,
|
||||
'newpass': 'newencrypt'})
|
||||
|
||||
self.assertEqual(
|
||||
'Could not change passphrase: oops', str(error))
|
||||
|
||||
self.assertEqual(privateRSA_openssh, FilePath(filename).getContent())
|
||||
|
||||
|
||||
def test_changePassphraseEmptyStringError(self):
|
||||
"""
|
||||
L{changePassPhrase} doesn't modify the key file if C{toString} returns
|
||||
an empty string.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh)
|
||||
|
||||
def toString(*args, **kwargs):
|
||||
return ''
|
||||
|
||||
self.patch(Key, 'toString', toString)
|
||||
|
||||
error = self.assertRaises(
|
||||
SystemExit, changePassPhrase,
|
||||
{'filename': filename, 'newpass': 'newencrypt'})
|
||||
|
||||
if _PY3:
|
||||
expected = (
|
||||
"Could not change passphrase: cannot guess the type of b''")
|
||||
else:
|
||||
expected = (
|
||||
"Could not change passphrase: cannot guess the type of ''")
|
||||
self.assertEqual(expected, str(error))
|
||||
|
||||
self.assertEqual(privateRSA_openssh, FilePath(filename).getContent())
|
||||
|
||||
|
||||
def test_changePassphrasePublicKey(self):
|
||||
"""
|
||||
L{changePassPhrase} exits when trying to change the passphrase on a
|
||||
public key, and doesn't change the file.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(publicRSA_openssh)
|
||||
error = self.assertRaises(
|
||||
SystemExit, changePassPhrase,
|
||||
{'filename': filename, 'newpass': 'pass'})
|
||||
self.assertEqual(
|
||||
'Could not change passphrase: key not encrypted', str(error))
|
||||
self.assertEqual(publicRSA_openssh, FilePath(filename).getContent())
|
||||
@@ -0,0 +1,813 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_conch -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
import os, sys, socket
|
||||
import subprocess
|
||||
from itertools import count
|
||||
|
||||
from zope.interface import implementer
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.conch.error import ConchError
|
||||
from twisted.cred import portal
|
||||
from twisted.internet import reactor, defer, protocol
|
||||
from twisted.internet.error import ProcessExitedAlready
|
||||
from twisted.internet.task import LoopingCall
|
||||
from twisted.internet.utils import getProcessValue
|
||||
from twisted.python import filepath, log, runtime
|
||||
from twisted.python.compat import unicode, _PYPY
|
||||
from twisted.trial import unittest
|
||||
from twisted.conch.test.test_ssh import ConchTestRealm
|
||||
from twisted.python.procutils import which
|
||||
|
||||
from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh
|
||||
from twisted.conch.test.keydata import publicDSA_openssh, privateDSA_openssh
|
||||
|
||||
try:
|
||||
from twisted.conch.test.test_ssh import ConchTestServerFactory, \
|
||||
conchTestPublicKeyChecker
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import pyasn1
|
||||
except ImportError:
|
||||
pyasn1 = None
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
if cryptography:
|
||||
from twisted.conch.avatar import ConchUser
|
||||
from twisted.conch.ssh.session import ISession, SSHSession, wrapProtocol
|
||||
else:
|
||||
from twisted.conch.interfaces import ISession
|
||||
|
||||
class ConchUser:
|
||||
pass
|
||||
try:
|
||||
from twisted.conch.scripts.conch import (
|
||||
SSHSession as StdioInteractingSession
|
||||
)
|
||||
except ImportError as e:
|
||||
StdioInteractingSession = None
|
||||
_reason = str(e)
|
||||
del e
|
||||
|
||||
|
||||
|
||||
def _has_ipv6():
|
||||
""" Returns True if the system can bind an IPv6 address."""
|
||||
sock = None
|
||||
has_ipv6 = False
|
||||
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET6)
|
||||
sock.bind(('::1', 0))
|
||||
has_ipv6 = True
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
if sock:
|
||||
sock.close()
|
||||
return has_ipv6
|
||||
|
||||
|
||||
HAS_IPV6 = _has_ipv6()
|
||||
|
||||
|
||||
class FakeStdio(object):
|
||||
"""
|
||||
A fake for testing L{twisted.conch.scripts.conch.SSHSession.eofReceived} and
|
||||
L{twisted.conch.scripts.cftp.SSHSession.eofReceived}.
|
||||
|
||||
@ivar writeConnLost: A flag which records whether L{loserWriteConnection}
|
||||
has been called.
|
||||
"""
|
||||
writeConnLost = False
|
||||
|
||||
def loseWriteConnection(self):
|
||||
"""
|
||||
Record the call to loseWriteConnection.
|
||||
"""
|
||||
self.writeConnLost = True
|
||||
|
||||
|
||||
|
||||
class StdioInteractingSessionTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{twisted.conch.scripts.conch.SSHSession}.
|
||||
"""
|
||||
if StdioInteractingSession is None:
|
||||
skip = _reason
|
||||
|
||||
|
||||
def test_eofReceived(self):
|
||||
"""
|
||||
L{twisted.conch.scripts.conch.SSHSession.eofReceived} loses the
|
||||
write half of its stdio connection.
|
||||
"""
|
||||
stdio = FakeStdio()
|
||||
channel = StdioInteractingSession()
|
||||
channel.stdio = stdio
|
||||
channel.eofReceived()
|
||||
self.assertTrue(stdio.writeConnLost)
|
||||
|
||||
|
||||
|
||||
class Echo(protocol.Protocol):
|
||||
def connectionMade(self):
|
||||
log.msg('ECHO CONNECTION MADE')
|
||||
|
||||
|
||||
def connectionLost(self, reason):
|
||||
log.msg('ECHO CONNECTION DONE')
|
||||
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.transport.write(data)
|
||||
if b'\n' in data:
|
||||
self.transport.loseConnection()
|
||||
|
||||
|
||||
|
||||
class EchoFactory(protocol.Factory):
|
||||
protocol = Echo
|
||||
|
||||
|
||||
|
||||
class ConchTestOpenSSHProcess(protocol.ProcessProtocol):
|
||||
"""
|
||||
Test protocol for launching an OpenSSH client process.
|
||||
|
||||
@ivar deferred: Set by whatever uses this object. Accessed using
|
||||
L{_getDeferred}, which destroys the value so the Deferred is not
|
||||
fired twice. Fires when the process is terminated.
|
||||
"""
|
||||
|
||||
deferred = None
|
||||
buf = b''
|
||||
|
||||
def _getDeferred(self):
|
||||
d, self.deferred = self.deferred, None
|
||||
return d
|
||||
|
||||
|
||||
def outReceived(self, data):
|
||||
self.buf += data
|
||||
|
||||
|
||||
def processEnded(self, reason):
|
||||
"""
|
||||
Called when the process has ended.
|
||||
|
||||
@param reason: a Failure giving the reason for the process' end.
|
||||
"""
|
||||
if reason.value.exitCode != 0:
|
||||
self._getDeferred().errback(
|
||||
ConchError("exit code was not 0: {}".format(
|
||||
reason.value.exitCode)))
|
||||
else:
|
||||
buf = self.buf.replace(b'\r\n', b'\n')
|
||||
self._getDeferred().callback(buf)
|
||||
|
||||
|
||||
|
||||
class ConchTestForwardingProcess(protocol.ProcessProtocol):
|
||||
"""
|
||||
Manages a third-party process which launches a server.
|
||||
|
||||
Uses L{ConchTestForwardingPort} to connect to the third-party server.
|
||||
Once L{ConchTestForwardingPort} has disconnected, kill the process and fire
|
||||
a Deferred with the data received by the L{ConchTestForwardingPort}.
|
||||
|
||||
@ivar deferred: Set by whatever uses this object. Accessed using
|
||||
L{_getDeferred}, which destroys the value so the Deferred is not
|
||||
fired twice. Fires when the process is terminated.
|
||||
"""
|
||||
|
||||
deferred = None
|
||||
|
||||
def __init__(self, port, data):
|
||||
"""
|
||||
@type port: L{int}
|
||||
@param port: The port on which the third-party server is listening.
|
||||
(it is assumed that the server is running on localhost).
|
||||
|
||||
@type data: L{str}
|
||||
@param data: This is sent to the third-party server. Must end with '\n'
|
||||
in order to trigger a disconnect.
|
||||
"""
|
||||
self.port = port
|
||||
self.buffer = None
|
||||
self.data = data
|
||||
|
||||
|
||||
def _getDeferred(self):
|
||||
d, self.deferred = self.deferred, None
|
||||
return d
|
||||
|
||||
|
||||
def connectionMade(self):
|
||||
self._connect()
|
||||
|
||||
|
||||
def _connect(self):
|
||||
"""
|
||||
Connect to the server, which is often a third-party process.
|
||||
Tries to reconnect if it fails because we have no way of determining
|
||||
exactly when the port becomes available for listening -- we can only
|
||||
know when the process starts.
|
||||
"""
|
||||
cc = protocol.ClientCreator(reactor, ConchTestForwardingPort, self,
|
||||
self.data)
|
||||
d = cc.connectTCP('127.0.0.1', self.port)
|
||||
d.addErrback(self._ebConnect)
|
||||
return d
|
||||
|
||||
|
||||
def _ebConnect(self, f):
|
||||
reactor.callLater(.1, self._connect)
|
||||
|
||||
|
||||
def forwardingPortDisconnected(self, buffer):
|
||||
"""
|
||||
The network connection has died; save the buffer of output
|
||||
from the network and attempt to quit the process gracefully,
|
||||
and then (after the reactor has spun) send it a KILL signal.
|
||||
"""
|
||||
self.buffer = buffer
|
||||
self.transport.write(b'\x03')
|
||||
self.transport.loseConnection()
|
||||
reactor.callLater(0, self._reallyDie)
|
||||
|
||||
|
||||
def _reallyDie(self):
|
||||
try:
|
||||
self.transport.signalProcess('KILL')
|
||||
except ProcessExitedAlready:
|
||||
pass
|
||||
|
||||
|
||||
def processEnded(self, reason):
|
||||
"""
|
||||
Fire the Deferred at self.deferred with the data collected
|
||||
from the L{ConchTestForwardingPort} connection, if any.
|
||||
"""
|
||||
self._getDeferred().callback(self.buffer)
|
||||
|
||||
|
||||
|
||||
class ConchTestForwardingPort(protocol.Protocol):
|
||||
"""
|
||||
Connects to server launched by a third-party process (managed by
|
||||
L{ConchTestForwardingProcess}) sends data, then reports whatever it
|
||||
received back to the L{ConchTestForwardingProcess} once the connection
|
||||
is ended.
|
||||
"""
|
||||
|
||||
def __init__(self, protocol, data):
|
||||
"""
|
||||
@type protocol: L{ConchTestForwardingProcess}
|
||||
@param protocol: The L{ProcessProtocol} which made this connection.
|
||||
|
||||
@type data: str
|
||||
@param data: The data to be sent to the third-party server.
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.data = data
|
||||
|
||||
|
||||
def connectionMade(self):
|
||||
self.buffer = b''
|
||||
self.transport.write(self.data)
|
||||
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buffer += data
|
||||
|
||||
|
||||
def connectionLost(self, reason):
|
||||
self.protocol.forwardingPortDisconnected(self.buffer)
|
||||
|
||||
|
||||
|
||||
def _makeArgs(args, mod="conch"):
|
||||
start = [sys.executable, '-c'
|
||||
"""
|
||||
### Twisted Preamble
|
||||
import sys, os
|
||||
path = os.path.abspath(sys.argv[0])
|
||||
while os.path.dirname(path) != path:
|
||||
if os.path.basename(path).startswith('Twisted'):
|
||||
sys.path.insert(0, path)
|
||||
break
|
||||
path = os.path.dirname(path)
|
||||
|
||||
from twisted.conch.scripts.%s import run
|
||||
run()""" % mod]
|
||||
madeArgs = []
|
||||
for arg in start + list(args):
|
||||
if isinstance(arg, unicode):
|
||||
arg = arg.encode("utf-8")
|
||||
madeArgs.append(arg)
|
||||
return madeArgs
|
||||
|
||||
|
||||
|
||||
class ConchServerSetupMixin:
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not pyasn1:
|
||||
skip = "Cannot run without PyASN1"
|
||||
|
||||
# FIXME: https://twistedmatrix.com/trac/ticket/8506
|
||||
|
||||
# This should be un-skipped on Travis after the ticket is fixed. For now
|
||||
# is enabled so that we can continue with fixing other stuff using Travis.
|
||||
if _PYPY:
|
||||
skip = 'PyPy known_host not working yet on Travis.'
|
||||
|
||||
realmFactory = staticmethod(lambda: ConchTestRealm(b'testuser'))
|
||||
|
||||
def _createFiles(self):
|
||||
for f in ['rsa_test','rsa_test.pub','dsa_test','dsa_test.pub',
|
||||
'kh_test']:
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
with open('rsa_test','wb') as f:
|
||||
f.write(privateRSA_openssh)
|
||||
with open('rsa_test.pub','wb') as f:
|
||||
f.write(publicRSA_openssh)
|
||||
with open('dsa_test.pub','wb') as f:
|
||||
f.write(publicDSA_openssh)
|
||||
with open('dsa_test','wb') as f:
|
||||
f.write(privateDSA_openssh)
|
||||
os.chmod('dsa_test', 33152)
|
||||
os.chmod('rsa_test', 33152)
|
||||
with open('kh_test','wb') as f:
|
||||
f.write(b'127.0.0.1 '+publicRSA_openssh)
|
||||
|
||||
|
||||
def _getFreePort(self):
|
||||
s = socket.socket()
|
||||
s.bind(('', 0))
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
return port
|
||||
|
||||
|
||||
def _makeConchFactory(self):
|
||||
"""
|
||||
Make a L{ConchTestServerFactory}, which allows us to start a
|
||||
L{ConchTestServer} -- i.e. an actually listening conch.
|
||||
"""
|
||||
realm = self.realmFactory()
|
||||
p = portal.Portal(realm)
|
||||
p.registerChecker(conchTestPublicKeyChecker())
|
||||
factory = ConchTestServerFactory()
|
||||
factory.portal = p
|
||||
return factory
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self._createFiles()
|
||||
self.conchFactory = self._makeConchFactory()
|
||||
self.conchFactory.expectedLoseConnection = 1
|
||||
self.conchServer = reactor.listenTCP(0, self.conchFactory,
|
||||
interface="127.0.0.1")
|
||||
self.echoServer = reactor.listenTCP(0, EchoFactory())
|
||||
self.echoPort = self.echoServer.getHost().port
|
||||
if HAS_IPV6:
|
||||
self.echoServerV6 = reactor.listenTCP(0, EchoFactory(), interface="::1")
|
||||
self.echoPortV6 = self.echoServerV6.getHost().port
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
self.conchFactory.proto.done = 1
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.conchFactory.proto.transport.loseConnection()
|
||||
deferreds = [
|
||||
defer.maybeDeferred(self.conchServer.stopListening),
|
||||
defer.maybeDeferred(self.echoServer.stopListening),
|
||||
]
|
||||
if HAS_IPV6:
|
||||
deferreds.append(defer.maybeDeferred(self.echoServerV6.stopListening))
|
||||
return defer.gatherResults(deferreds)
|
||||
|
||||
|
||||
|
||||
class ForwardingMixin(ConchServerSetupMixin):
|
||||
"""
|
||||
Template class for tests of the Conch server's ability to forward arbitrary
|
||||
protocols over SSH.
|
||||
|
||||
These tests are integration tests, not unit tests. They launch a Conch
|
||||
server, a custom TCP server (just an L{EchoProtocol}) and then call
|
||||
L{execute}.
|
||||
|
||||
L{execute} is implemented by subclasses of L{ForwardingMixin}. It should
|
||||
cause an SSH client to connect to the Conch server, asking it to forward
|
||||
data to the custom TCP server.
|
||||
"""
|
||||
|
||||
def test_exec(self):
|
||||
"""
|
||||
Test that we can use whatever client to send the command "echo goodbye"
|
||||
to the Conch server. Make sure we receive "goodbye" back from the
|
||||
server.
|
||||
"""
|
||||
d = self.execute('echo goodbye', ConchTestOpenSSHProcess())
|
||||
return d.addCallback(self.assertEqual, b'goodbye\n')
|
||||
|
||||
|
||||
def test_localToRemoteForwarding(self):
|
||||
"""
|
||||
Test that we can use whatever client to forward a local port to a
|
||||
specified port on the server.
|
||||
"""
|
||||
localPort = self._getFreePort()
|
||||
process = ConchTestForwardingProcess(localPort, b'test\n')
|
||||
d = self.execute('', process,
|
||||
sshArgs='-N -L%i:127.0.0.1:%i'
|
||||
% (localPort, self.echoPort))
|
||||
d.addCallback(self.assertEqual, b'test\n')
|
||||
return d
|
||||
|
||||
|
||||
def test_remoteToLocalForwarding(self):
|
||||
"""
|
||||
Test that we can use whatever client to forward a port from the server
|
||||
to a port locally.
|
||||
"""
|
||||
localPort = self._getFreePort()
|
||||
process = ConchTestForwardingProcess(localPort, b'test\n')
|
||||
d = self.execute('', process,
|
||||
sshArgs='-N -R %i:127.0.0.1:%i'
|
||||
% (localPort, self.echoPort))
|
||||
d.addCallback(self.assertEqual, b'test\n')
|
||||
return d
|
||||
|
||||
|
||||
|
||||
# Conventionally there is a separate adapter object which provides ISession for
|
||||
# the user, but making the user provide ISession directly works too. This isn't
|
||||
# a full implementation of ISession though, just enough to make these tests
|
||||
# pass.
|
||||
@implementer(ISession)
|
||||
class RekeyAvatar(ConchUser):
|
||||
"""
|
||||
This avatar implements a shell which sends 60 numbered lines to whatever
|
||||
connects to it, then closes the session with a 0 exit status.
|
||||
|
||||
60 lines is selected as being enough to send more than 2kB of traffic, the
|
||||
amount the client is configured to initiate a rekey after.
|
||||
"""
|
||||
def __init__(self):
|
||||
ConchUser.__init__(self)
|
||||
self.channelLookup[b'session'] = SSHSession
|
||||
|
||||
|
||||
def openShell(self, transport):
|
||||
"""
|
||||
Write 60 lines of data to the transport, then exit.
|
||||
"""
|
||||
proto = protocol.Protocol()
|
||||
proto.makeConnection(transport)
|
||||
transport.makeConnection(wrapProtocol(proto))
|
||||
|
||||
# Send enough bytes to the connection so that a rekey is triggered in
|
||||
# the client.
|
||||
def write(counter):
|
||||
i = next(counter)
|
||||
if i == 60:
|
||||
call.stop()
|
||||
transport.session.conn.sendRequest(
|
||||
transport.session, b'exit-status', b'\x00\x00\x00\x00')
|
||||
transport.loseConnection()
|
||||
else:
|
||||
line = "line #%02d\n" % (i,)
|
||||
line = line.encode("utf-8")
|
||||
transport.write(line)
|
||||
|
||||
# The timing for this loop is an educated guess (and/or the result of
|
||||
# experimentation) to exercise the case where a packet is generated
|
||||
# mid-rekey. Since the other side of the connection is (so far) the
|
||||
# OpenSSH command line client, there's no easy way to determine when the
|
||||
# rekey has been initiated. If there were, then generating a packet
|
||||
# immediately at that time would be a better way to test the
|
||||
# functionality being tested here.
|
||||
call = LoopingCall(write, count())
|
||||
call.start(0.01)
|
||||
|
||||
|
||||
def closed(self):
|
||||
"""
|
||||
Ignore the close of the session.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class RekeyRealm:
|
||||
"""
|
||||
This realm gives out new L{RekeyAvatar} instances for any avatar request.
|
||||
"""
|
||||
def requestAvatar(self, avatarID, mind, *interfaces):
|
||||
return interfaces[0], RekeyAvatar(), lambda: None
|
||||
|
||||
|
||||
|
||||
class RekeyTestsMixin(ConchServerSetupMixin):
|
||||
"""
|
||||
TestCase mixin which defines tests exercising L{SSHTransportBase}'s handling
|
||||
of rekeying messages.
|
||||
"""
|
||||
realmFactory = RekeyRealm
|
||||
|
||||
def test_clientRekey(self):
|
||||
"""
|
||||
After a client-initiated rekey is completed, application data continues
|
||||
to be passed over the SSH connection.
|
||||
"""
|
||||
process = ConchTestOpenSSHProcess()
|
||||
d = self.execute("", process, '-o RekeyLimit=2K')
|
||||
def finished(result):
|
||||
expectedResult = '\n'.join(['line #%02d' % (i,) for i in range(60)]) + '\n'
|
||||
expectedResult = expectedResult.encode("utf-8")
|
||||
self.assertEqual(result, expectedResult)
|
||||
d.addCallback(finished)
|
||||
return d
|
||||
|
||||
|
||||
|
||||
class OpenSSHClientMixin:
|
||||
if not which('ssh'):
|
||||
skip = "no ssh command-line client available"
|
||||
|
||||
|
||||
def execute(self, remoteCommand, process, sshArgs=''):
|
||||
"""
|
||||
Connects to the SSH server started in L{ConchServerSetupMixin.setUp} by
|
||||
running the 'ssh' command line tool.
|
||||
|
||||
@type remoteCommand: str
|
||||
@param remoteCommand: The command (with arguments) to run on the
|
||||
remote end.
|
||||
|
||||
@type process: L{ConchTestOpenSSHProcess}
|
||||
|
||||
@type sshArgs: str
|
||||
@param sshArgs: Arguments to pass to the 'ssh' process.
|
||||
|
||||
@return: L{defer.Deferred}
|
||||
"""
|
||||
# PubkeyAcceptedKeyTypes does not exist prior to OpenSSH 7.0 so we
|
||||
# first need to check if we can set it. If we can, -V will just print
|
||||
# the version without doing anything else; if we can't, we will get a
|
||||
# configuration error.
|
||||
d = getProcessValue(
|
||||
which('ssh')[0], ('-o', 'PubkeyAcceptedKeyTypes=ssh-dss', '-V'))
|
||||
def hasPAKT(status):
|
||||
if status == 0:
|
||||
opts = '-oPubkeyAcceptedKeyTypes=ssh-dss '
|
||||
else:
|
||||
opts = ''
|
||||
|
||||
process.deferred = defer.Deferred()
|
||||
# Pass -F /dev/null to avoid the user's configuration file from
|
||||
# being loaded, as it may contain settings that cause our tests to
|
||||
# fail or hang.
|
||||
cmdline = ('ssh -2 -l testuser -p %i '
|
||||
'-F /dev/null '
|
||||
'-oUserKnownHostsFile=kh_test '
|
||||
'-oPasswordAuthentication=no '
|
||||
# Always use the RSA key, since that's the one in kh_test.
|
||||
'-oHostKeyAlgorithms=ssh-rsa '
|
||||
'-a '
|
||||
'-i dsa_test ') + opts + sshArgs + \
|
||||
' 127.0.0.1 ' + remoteCommand
|
||||
port = self.conchServer.getHost().port
|
||||
cmds = (cmdline % port).split()
|
||||
encodedCmds = []
|
||||
for cmd in cmds:
|
||||
if isinstance(cmd, unicode):
|
||||
cmd = cmd.encode("utf-8")
|
||||
encodedCmds.append(cmd)
|
||||
reactor.spawnProcess(process, which('ssh')[0], encodedCmds)
|
||||
return process.deferred
|
||||
return d.addCallback(hasPAKT)
|
||||
|
||||
|
||||
|
||||
class OpenSSHKeyExchangeTests(ConchServerSetupMixin, OpenSSHClientMixin,
|
||||
unittest.TestCase):
|
||||
"""
|
||||
Tests L{SSHTransportBase}'s key exchange algorithm compatibility with
|
||||
OpenSSH.
|
||||
"""
|
||||
|
||||
def assertExecuteWithKexAlgorithm(self, keyExchangeAlgo):
|
||||
"""
|
||||
Call execute() method of L{OpenSSHClientMixin} with an ssh option that
|
||||
forces the exclusive use of the key exchange algorithm specified by
|
||||
keyExchangeAlgo
|
||||
|
||||
@type keyExchangeAlgo: L{str}
|
||||
@param keyExchangeAlgo: The key exchange algorithm to use
|
||||
|
||||
@return: L{defer.Deferred}
|
||||
"""
|
||||
kexAlgorithms = []
|
||||
try:
|
||||
output = subprocess.check_output([which('ssh')[0], '-Q', 'kex'],
|
||||
stderr=subprocess.STDOUT)
|
||||
if not isinstance(output, str):
|
||||
output = output.decode("utf-8")
|
||||
kexAlgorithms = output.split()
|
||||
except:
|
||||
pass
|
||||
|
||||
if keyExchangeAlgo not in kexAlgorithms:
|
||||
raise unittest.SkipTest(
|
||||
"{} not supported by ssh client".format(
|
||||
keyExchangeAlgo))
|
||||
|
||||
d = self.execute('echo hello', ConchTestOpenSSHProcess(),
|
||||
'-oKexAlgorithms=' + keyExchangeAlgo)
|
||||
return d.addCallback(self.assertEqual, b'hello\n')
|
||||
|
||||
|
||||
def test_ECDHSHA256(self):
|
||||
"""
|
||||
The ecdh-sha2-nistp256 key exchange algorithm is compatible with
|
||||
OpenSSH
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm(
|
||||
'ecdh-sha2-nistp256')
|
||||
|
||||
|
||||
def test_ECDHSHA384(self):
|
||||
"""
|
||||
The ecdh-sha2-nistp384 key exchange algorithm is compatible with
|
||||
OpenSSH
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm(
|
||||
'ecdh-sha2-nistp384')
|
||||
|
||||
|
||||
def test_ECDHSHA521(self):
|
||||
"""
|
||||
The ecdh-sha2-nistp521 key exchange algorithm is compatible with
|
||||
OpenSSH
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm(
|
||||
'ecdh-sha2-nistp521')
|
||||
|
||||
|
||||
def test_DH_GROUP14(self):
|
||||
"""
|
||||
The diffie-hellman-group14-sha1 key exchange algorithm is compatible
|
||||
with OpenSSH.
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm(
|
||||
'diffie-hellman-group14-sha1')
|
||||
|
||||
|
||||
def test_DH_GROUP_EXCHANGE_SHA1(self):
|
||||
"""
|
||||
The diffie-hellman-group-exchange-sha1 key exchange algorithm is
|
||||
compatible with OpenSSH.
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm(
|
||||
'diffie-hellman-group-exchange-sha1')
|
||||
|
||||
|
||||
def test_DH_GROUP_EXCHANGE_SHA256(self):
|
||||
"""
|
||||
The diffie-hellman-group-exchange-sha256 key exchange algorithm is
|
||||
compatible with OpenSSH.
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm(
|
||||
'diffie-hellman-group-exchange-sha256')
|
||||
|
||||
|
||||
def test_unsupported_algorithm(self):
|
||||
"""
|
||||
The list of key exchange algorithms supported
|
||||
by OpenSSH client is obtained with C{ssh -Q kex}.
|
||||
"""
|
||||
self.assertRaises(unittest.SkipTest,
|
||||
self.assertExecuteWithKexAlgorithm,
|
||||
'unsupported-algorithm')
|
||||
|
||||
|
||||
|
||||
class OpenSSHClientForwardingTests(ForwardingMixin, OpenSSHClientMixin,
|
||||
unittest.TestCase):
|
||||
"""
|
||||
Connection forwarding tests run against the OpenSSL command line client.
|
||||
"""
|
||||
def test_localToRemoteForwardingV6(self):
|
||||
"""
|
||||
Forwarding of arbitrary IPv6 TCP connections via SSH.
|
||||
"""
|
||||
localPort = self._getFreePort()
|
||||
process = ConchTestForwardingProcess(localPort, b'test\n')
|
||||
d = self.execute('', process,
|
||||
sshArgs='-N -L%i:[::1]:%i'
|
||||
% (localPort, self.echoPortV6))
|
||||
d.addCallback(self.assertEqual, b'test\n')
|
||||
return d
|
||||
if not HAS_IPV6:
|
||||
test_localToRemoteForwardingV6.skip = "Requires IPv6 support"
|
||||
|
||||
|
||||
|
||||
class OpenSSHClientRekeyTests(RekeyTestsMixin, OpenSSHClientMixin,
|
||||
unittest.TestCase):
|
||||
"""
|
||||
Rekeying tests run against the OpenSSL command line client.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class CmdLineClientTests(ForwardingMixin, unittest.TestCase):
|
||||
"""
|
||||
Connection forwarding tests run against the Conch command line client.
|
||||
"""
|
||||
if runtime.platformType == 'win32':
|
||||
skip = "can't run cmdline client on win32"
|
||||
|
||||
|
||||
def execute(self, remoteCommand, process, sshArgs='', conchArgs=None):
|
||||
"""
|
||||
As for L{OpenSSHClientTestCase.execute}, except it runs the 'conch'
|
||||
command line tool, not 'ssh'.
|
||||
"""
|
||||
if conchArgs is None:
|
||||
conchArgs = []
|
||||
|
||||
process.deferred = defer.Deferred()
|
||||
port = self.conchServer.getHost().port
|
||||
cmd = ('-p {} -l testuser '
|
||||
'--known-hosts kh_test '
|
||||
'--user-authentications publickey '
|
||||
'-a '
|
||||
'-i dsa_test '
|
||||
'-v '.format(port) + sshArgs +
|
||||
' 127.0.0.1 ' + remoteCommand)
|
||||
cmds = _makeArgs(conchArgs + cmd.split())
|
||||
env = os.environ.copy()
|
||||
env['PYTHONPATH'] = os.pathsep.join(sys.path)
|
||||
encodedCmds = []
|
||||
encodedEnv = {}
|
||||
for cmd in cmds:
|
||||
if isinstance(cmd, unicode):
|
||||
cmd = cmd.encode("utf-8")
|
||||
encodedCmds.append(cmd)
|
||||
for var in env:
|
||||
val = env[var]
|
||||
if isinstance(var, unicode):
|
||||
var = var.encode("utf-8")
|
||||
if isinstance(val, unicode):
|
||||
val = val.encode("utf-8")
|
||||
encodedEnv[var] = val
|
||||
reactor.spawnProcess(process, sys.executable, encodedCmds, env=encodedEnv)
|
||||
return process.deferred
|
||||
|
||||
|
||||
def test_runWithLogFile(self):
|
||||
"""
|
||||
It can store logs to a local file.
|
||||
"""
|
||||
def cb_check_log(result):
|
||||
logContent = logPath.getContent()
|
||||
self.assertIn(b'Log opened.', logContent)
|
||||
|
||||
logPath = filepath.FilePath(self.mktemp())
|
||||
|
||||
d = self.execute(
|
||||
remoteCommand='echo goodbye',
|
||||
process=ConchTestOpenSSHProcess(),
|
||||
conchArgs=['--log', '--logfile', logPath.path,
|
||||
'--host-key-algorithms', 'ssh-rsa']
|
||||
)
|
||||
|
||||
d.addCallback(self.assertEqual, b'goodbye\n')
|
||||
d.addCallback(cb_check_log)
|
||||
return d
|
||||
|
||||
|
||||
def test_runWithNoHostAlgorithmsSpecified(self):
|
||||
"""
|
||||
Do not use --host-key-algorithms flag on command line.
|
||||
"""
|
||||
d = self.execute(
|
||||
remoteCommand='echo goodbye',
|
||||
process=ConchTestOpenSSHProcess()
|
||||
)
|
||||
|
||||
d.addCallback(self.assertEqual, b'goodbye\n')
|
||||
return d
|
||||
@@ -0,0 +1,761 @@
|
||||
# Copyright (c) 2007-2010 Twisted Matrix Laboratories.
|
||||
# See LICENSE for details
|
||||
|
||||
"""
|
||||
This module tests twisted.conch.ssh.connection.
|
||||
"""
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
|
||||
from twisted.conch import error
|
||||
if cryptography:
|
||||
from twisted.conch.ssh import common, connection
|
||||
else:
|
||||
class connection:
|
||||
class SSHConnection: pass
|
||||
|
||||
from twisted.conch.ssh import channel
|
||||
from twisted.python.compat import long
|
||||
from twisted.trial import unittest
|
||||
from twisted.conch.test import test_userauth
|
||||
|
||||
|
||||
class TestChannel(channel.SSHChannel):
|
||||
"""
|
||||
A mocked-up version of twisted.conch.ssh.channel.SSHChannel.
|
||||
|
||||
@ivar gotOpen: True if channelOpen has been called.
|
||||
@type gotOpen: L{bool}
|
||||
@ivar specificData: the specific channel open data passed to channelOpen.
|
||||
@type specificData: L{bytes}
|
||||
@ivar openFailureReason: the reason passed to openFailed.
|
||||
@type openFailed: C{error.ConchError}
|
||||
@ivar inBuffer: a C{list} of strings received by the channel.
|
||||
@type inBuffer: C{list}
|
||||
@ivar extBuffer: a C{list} of 2-tuples (type, extended data) of received by
|
||||
the channel.
|
||||
@type extBuffer: C{list}
|
||||
@ivar numberRequests: the number of requests that have been made to this
|
||||
channel.
|
||||
@type numberRequests: L{int}
|
||||
@ivar gotEOF: True if the other side sent EOF.
|
||||
@type gotEOF: L{bool}
|
||||
@ivar gotOneClose: True if the other side closed the connection.
|
||||
@type gotOneClose: L{bool}
|
||||
@ivar gotClosed: True if the channel is closed.
|
||||
@type gotClosed: L{bool}
|
||||
"""
|
||||
name = b"TestChannel"
|
||||
gotOpen = False
|
||||
gotClosed = False
|
||||
|
||||
def logPrefix(self):
|
||||
return "TestChannel %i" % self.id
|
||||
|
||||
def channelOpen(self, specificData):
|
||||
"""
|
||||
The channel is open. Set up the instance variables.
|
||||
"""
|
||||
self.gotOpen = True
|
||||
self.specificData = specificData
|
||||
self.inBuffer = []
|
||||
self.extBuffer = []
|
||||
self.numberRequests = 0
|
||||
self.gotEOF = False
|
||||
self.gotOneClose = False
|
||||
self.gotClosed = False
|
||||
|
||||
def openFailed(self, reason):
|
||||
"""
|
||||
Opening the channel failed. Store the reason why.
|
||||
"""
|
||||
self.openFailureReason = reason
|
||||
|
||||
def request_test(self, data):
|
||||
"""
|
||||
A test request. Return True if data is 'data'.
|
||||
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
self.numberRequests += 1
|
||||
return data == b'data'
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
Data was received. Store it in the buffer.
|
||||
"""
|
||||
self.inBuffer.append(data)
|
||||
|
||||
def extReceived(self, code, data):
|
||||
"""
|
||||
Extended data was received. Store it in the buffer.
|
||||
"""
|
||||
self.extBuffer.append((code, data))
|
||||
|
||||
def eofReceived(self):
|
||||
"""
|
||||
EOF was received. Remember it.
|
||||
"""
|
||||
self.gotEOF = True
|
||||
|
||||
def closeReceived(self):
|
||||
"""
|
||||
Close was received. Remember it.
|
||||
"""
|
||||
self.gotOneClose = True
|
||||
|
||||
def closed(self):
|
||||
"""
|
||||
The channel is closed. Rembember it.
|
||||
"""
|
||||
self.gotClosed = True
|
||||
|
||||
class TestAvatar:
|
||||
"""
|
||||
A mocked-up version of twisted.conch.avatar.ConchUser
|
||||
"""
|
||||
_ARGS_ERROR_CODE = 123
|
||||
|
||||
def lookupChannel(self, channelType, windowSize, maxPacket, data):
|
||||
"""
|
||||
The server wants us to return a channel. If the requested channel is
|
||||
our TestChannel, return it, otherwise return None.
|
||||
"""
|
||||
if channelType == TestChannel.name:
|
||||
return TestChannel(remoteWindow=windowSize,
|
||||
remoteMaxPacket=maxPacket,
|
||||
data=data, avatar=self)
|
||||
elif channelType == b"conch-error-args":
|
||||
# Raise a ConchError with backwards arguments to make sure the
|
||||
# connection fixes it for us. This case should be deprecated and
|
||||
# deleted eventually, but only after all of Conch gets the argument
|
||||
# order right.
|
||||
raise error.ConchError(
|
||||
self._ARGS_ERROR_CODE, "error args in wrong order")
|
||||
|
||||
|
||||
def gotGlobalRequest(self, requestType, data):
|
||||
"""
|
||||
The client has made a global request. If the global request is
|
||||
'TestGlobal', return True. If the global request is 'TestData',
|
||||
return True and the request-specific data we received. Otherwise,
|
||||
return False.
|
||||
"""
|
||||
if requestType == b'TestGlobal':
|
||||
return True
|
||||
elif requestType == b'TestData':
|
||||
return True, data
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class TestConnection(connection.SSHConnection):
|
||||
"""
|
||||
A subclass of SSHConnection for testing.
|
||||
|
||||
@ivar channel: the current channel.
|
||||
@type channel. C{TestChannel}
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
def logPrefix(self):
|
||||
return "TestConnection"
|
||||
|
||||
def global_TestGlobal(self, data):
|
||||
"""
|
||||
The other side made the 'TestGlobal' global request. Return True.
|
||||
"""
|
||||
return True
|
||||
|
||||
def global_Test_Data(self, data):
|
||||
"""
|
||||
The other side made the 'Test-Data' global request. Return True and
|
||||
the data we received.
|
||||
"""
|
||||
return True, data
|
||||
|
||||
def channel_TestChannel(self, windowSize, maxPacket, data):
|
||||
"""
|
||||
The other side is requesting the TestChannel. Create a C{TestChannel}
|
||||
instance, store it, and return it.
|
||||
"""
|
||||
self.channel = TestChannel(remoteWindow=windowSize,
|
||||
remoteMaxPacket=maxPacket, data=data)
|
||||
return self.channel
|
||||
|
||||
def channel_ErrorChannel(self, windowSize, maxPacket, data):
|
||||
"""
|
||||
The other side is requesting the ErrorChannel. Raise an exception.
|
||||
"""
|
||||
raise AssertionError('no such thing')
|
||||
|
||||
|
||||
|
||||
class ConnectionTests(unittest.TestCase):
|
||||
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
if test_userauth.transport is None:
|
||||
skip = "Cannot run without both cryptography and pyasn1"
|
||||
|
||||
def setUp(self):
|
||||
self.transport = test_userauth.FakeTransport(None)
|
||||
self.transport.avatar = TestAvatar()
|
||||
self.conn = TestConnection()
|
||||
self.conn.transport = self.transport
|
||||
self.conn.serviceStarted()
|
||||
|
||||
def _openChannel(self, channel):
|
||||
"""
|
||||
Open the channel with the default connection.
|
||||
"""
|
||||
self.conn.openChannel(channel)
|
||||
self.transport.packets = self.transport.packets[:-1]
|
||||
self.conn.ssh_CHANNEL_OPEN_CONFIRMATION(struct.pack('>2L',
|
||||
channel.id, 255) + b'\x00\x02\x00\x00\x00\x00\x80\x00')
|
||||
|
||||
def tearDown(self):
|
||||
self.conn.serviceStopped()
|
||||
|
||||
def test_linkAvatar(self):
|
||||
"""
|
||||
Test that the connection links itself to the avatar in the
|
||||
transport.
|
||||
"""
|
||||
self.assertIs(self.transport.avatar.conn, self.conn)
|
||||
|
||||
def test_serviceStopped(self):
|
||||
"""
|
||||
Test that serviceStopped() closes any open channels.
|
||||
"""
|
||||
channel1 = TestChannel()
|
||||
channel2 = TestChannel()
|
||||
self.conn.openChannel(channel1)
|
||||
self.conn.openChannel(channel2)
|
||||
self.conn.ssh_CHANNEL_OPEN_CONFIRMATION(b'\x00\x00\x00\x00' * 4)
|
||||
self.assertTrue(channel1.gotOpen)
|
||||
self.assertFalse(channel1.gotClosed)
|
||||
self.assertFalse(channel2.gotOpen)
|
||||
self.assertFalse(channel2.gotClosed)
|
||||
self.conn.serviceStopped()
|
||||
self.assertTrue(channel1.gotClosed)
|
||||
self.assertFalse(channel2.gotOpen)
|
||||
self.assertFalse(channel2.gotClosed)
|
||||
from twisted.internet.error import ConnectionLost
|
||||
self.assertIsInstance(channel2.openFailureReason,
|
||||
ConnectionLost)
|
||||
|
||||
def test_GLOBAL_REQUEST(self):
|
||||
"""
|
||||
Test that global request packets are dispatched to the global_*
|
||||
methods and the return values are translated into success or failure
|
||||
messages.
|
||||
"""
|
||||
self.conn.ssh_GLOBAL_REQUEST(common.NS(b'TestGlobal') + b'\xff')
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_REQUEST_SUCCESS, b'')])
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_GLOBAL_REQUEST(common.NS(b'TestData') + b'\xff' +
|
||||
b'test data')
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_REQUEST_SUCCESS, b'test data')])
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_GLOBAL_REQUEST(common.NS(b'TestBad') + b'\xff')
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_REQUEST_FAILURE, b'')])
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_GLOBAL_REQUEST(common.NS(b'TestGlobal') + b'\x00')
|
||||
self.assertEqual(self.transport.packets, [])
|
||||
|
||||
def test_REQUEST_SUCCESS(self):
|
||||
"""
|
||||
Test that global request success packets cause the Deferred to be
|
||||
called back.
|
||||
"""
|
||||
d = self.conn.sendGlobalRequest(b'request', b'data', True)
|
||||
self.conn.ssh_REQUEST_SUCCESS(b'data')
|
||||
def check(data):
|
||||
self.assertEqual(data, b'data')
|
||||
d.addCallback(check)
|
||||
d.addErrback(self.fail)
|
||||
return d
|
||||
|
||||
def test_REQUEST_FAILURE(self):
|
||||
"""
|
||||
Test that global request failure packets cause the Deferred to be
|
||||
erred back.
|
||||
"""
|
||||
d = self.conn.sendGlobalRequest(b'request', b'data', True)
|
||||
self.conn.ssh_REQUEST_FAILURE(b'data')
|
||||
def check(f):
|
||||
self.assertEqual(f.value.data, b'data')
|
||||
d.addCallback(self.fail)
|
||||
d.addErrback(check)
|
||||
return d
|
||||
|
||||
def test_CHANNEL_OPEN(self):
|
||||
"""
|
||||
Test that open channel packets cause a channel to be created and
|
||||
opened or a failure message to be returned.
|
||||
"""
|
||||
del self.transport.avatar
|
||||
self.conn.ssh_CHANNEL_OPEN(common.NS(b'TestChannel') +
|
||||
b'\x00\x00\x00\x01' * 4)
|
||||
self.assertTrue(self.conn.channel.gotOpen)
|
||||
self.assertEqual(self.conn.channel.conn, self.conn)
|
||||
self.assertEqual(self.conn.channel.data, b'\x00\x00\x00\x01')
|
||||
self.assertEqual(self.conn.channel.specificData, b'\x00\x00\x00\x01')
|
||||
self.assertEqual(self.conn.channel.remoteWindowLeft, 1)
|
||||
self.assertEqual(self.conn.channel.remoteMaxPacket, 1)
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_OPEN_CONFIRMATION,
|
||||
b'\x00\x00\x00\x01\x00\x00\x00\x00\x00\x02\x00\x00'
|
||||
b'\x00\x00\x80\x00')])
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_CHANNEL_OPEN(common.NS(b'BadChannel') +
|
||||
b'\x00\x00\x00\x02' * 4)
|
||||
self.flushLoggedErrors()
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_OPEN_FAILURE,
|
||||
b'\x00\x00\x00\x02\x00\x00\x00\x03' + common.NS(
|
||||
b'unknown channel') + common.NS(b''))])
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_CHANNEL_OPEN(common.NS(b'ErrorChannel') +
|
||||
b'\x00\x00\x00\x02' * 4)
|
||||
self.flushLoggedErrors()
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_OPEN_FAILURE,
|
||||
b'\x00\x00\x00\x02\x00\x00\x00\x02' + common.NS(
|
||||
b'unknown failure') + common.NS(b''))])
|
||||
|
||||
|
||||
def _lookupChannelErrorTest(self, code):
|
||||
"""
|
||||
Deliver a request for a channel open which will result in an exception
|
||||
being raised during channel lookup. Assert that an error response is
|
||||
delivered as a result.
|
||||
"""
|
||||
self.transport.avatar._ARGS_ERROR_CODE = code
|
||||
self.conn.ssh_CHANNEL_OPEN(
|
||||
common.NS(b'conch-error-args') + b'\x00\x00\x00\x01' * 4)
|
||||
errors = self.flushLoggedErrors(error.ConchError)
|
||||
self.assertEqual(
|
||||
len(errors), 1, "Expected one error, got: %r" % (errors,))
|
||||
self.assertEqual(errors[0].value.args, (long(123), "error args in wrong order"))
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_OPEN_FAILURE,
|
||||
# The response includes some bytes which identifying the
|
||||
# associated request, as well as the error code (7b in hex) and
|
||||
# the error message.
|
||||
b'\x00\x00\x00\x01\x00\x00\x00\x7b' + common.NS(
|
||||
b'error args in wrong order') + common.NS(b''))])
|
||||
|
||||
|
||||
def test_lookupChannelError(self):
|
||||
"""
|
||||
If a C{lookupChannel} implementation raises L{error.ConchError} with the
|
||||
arguments in the wrong order, a C{MSG_CHANNEL_OPEN} failure is still
|
||||
sent in response to the message.
|
||||
|
||||
This is a temporary work-around until L{error.ConchError} is given
|
||||
better attributes and all of the Conch code starts constructing
|
||||
instances of it properly. Eventually this functionality should be
|
||||
deprecated and then removed.
|
||||
"""
|
||||
self._lookupChannelErrorTest(123)
|
||||
|
||||
|
||||
def test_lookupChannelErrorLongCode(self):
|
||||
"""
|
||||
Like L{test_lookupChannelError}, but for the case where the failure code
|
||||
is represented as a L{long} instead of a L{int}.
|
||||
"""
|
||||
self._lookupChannelErrorTest(long(123))
|
||||
|
||||
|
||||
def test_CHANNEL_OPEN_CONFIRMATION(self):
|
||||
"""
|
||||
Test that channel open confirmation packets cause the channel to be
|
||||
notified that it's open.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self.conn.openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_OPEN_CONFIRMATION(b'\x00\x00\x00\x00'*5)
|
||||
self.assertEqual(channel.remoteWindowLeft, 0)
|
||||
self.assertEqual(channel.remoteMaxPacket, 0)
|
||||
self.assertEqual(channel.specificData, b'\x00\x00\x00\x00')
|
||||
self.assertEqual(self.conn.channelsToRemoteChannel[channel],
|
||||
0)
|
||||
self.assertEqual(self.conn.localToRemoteChannel[0], 0)
|
||||
|
||||
def test_CHANNEL_OPEN_FAILURE(self):
|
||||
"""
|
||||
Test that channel open failure packets cause the channel to be
|
||||
notified that its opening failed.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self.conn.openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_OPEN_FAILURE(b'\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x01' + common.NS(b'failure!'))
|
||||
self.assertEqual(channel.openFailureReason.args, (b'failure!', 1))
|
||||
self.assertIsNone(self.conn.channels.get(channel))
|
||||
|
||||
|
||||
def test_CHANNEL_WINDOW_ADJUST(self):
|
||||
"""
|
||||
Test that channel window adjust messages add bytes to the channel
|
||||
window.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
oldWindowSize = channel.remoteWindowLeft
|
||||
self.conn.ssh_CHANNEL_WINDOW_ADJUST(b'\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x01')
|
||||
self.assertEqual(channel.remoteWindowLeft, oldWindowSize + 1)
|
||||
|
||||
def test_CHANNEL_DATA(self):
|
||||
"""
|
||||
Test that channel data messages are passed up to the channel, or
|
||||
cause the channel to be closed if the data is too large.
|
||||
"""
|
||||
channel = TestChannel(localWindow=6, localMaxPacket=5)
|
||||
self._openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_DATA(b'\x00\x00\x00\x00' + common.NS(b'data'))
|
||||
self.assertEqual(channel.inBuffer, [b'data'])
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_WINDOW_ADJUST, b'\x00\x00\x00\xff'
|
||||
b'\x00\x00\x00\x04')])
|
||||
self.transport.packets = []
|
||||
longData = b'a' * (channel.localWindowLeft + 1)
|
||||
self.conn.ssh_CHANNEL_DATA(b'\x00\x00\x00\x00' + common.NS(longData))
|
||||
self.assertEqual(channel.inBuffer, [b'data'])
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b'\x00\x00\x00\xff')])
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
bigData = b'a' * (channel.localMaxPacket + 1)
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_CHANNEL_DATA(b'\x00\x00\x00\x01' + common.NS(bigData))
|
||||
self.assertEqual(channel.inBuffer, [])
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b'\x00\x00\x00\xff')])
|
||||
|
||||
def test_CHANNEL_EXTENDED_DATA(self):
|
||||
"""
|
||||
Test that channel extended data messages are passed up to the channel,
|
||||
or cause the channel to be closed if they're too big.
|
||||
"""
|
||||
channel = TestChannel(localWindow=6, localMaxPacket=5)
|
||||
self._openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_EXTENDED_DATA(b'\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00' + common.NS(b'data'))
|
||||
self.assertEqual(channel.extBuffer, [(0, b'data')])
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_WINDOW_ADJUST, b'\x00\x00\x00\xff'
|
||||
b'\x00\x00\x00\x04')])
|
||||
self.transport.packets = []
|
||||
longData = b'a' * (channel.localWindowLeft + 1)
|
||||
self.conn.ssh_CHANNEL_EXTENDED_DATA(b'\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00' + common.NS(longData))
|
||||
self.assertEqual(channel.extBuffer, [(0, b'data')])
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b'\x00\x00\x00\xff')])
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
bigData = b'a' * (channel.localMaxPacket + 1)
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_CHANNEL_EXTENDED_DATA(b'\x00\x00\x00\x01\x00\x00\x00'
|
||||
b'\x00' + common.NS(bigData))
|
||||
self.assertEqual(channel.extBuffer, [])
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b'\x00\x00\x00\xff')])
|
||||
|
||||
def test_CHANNEL_EOF(self):
|
||||
"""
|
||||
Test that channel eof messages are passed up to the channel.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_EOF(b'\x00\x00\x00\x00')
|
||||
self.assertTrue(channel.gotEOF)
|
||||
|
||||
def test_CHANNEL_CLOSE(self):
|
||||
"""
|
||||
Test that channel close messages are passed up to the channel. Also,
|
||||
test that channel.close() is called if both sides are closed when this
|
||||
message is received.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.assertTrue(channel.gotOpen)
|
||||
self.assertFalse(channel.gotOneClose)
|
||||
self.assertFalse(channel.gotClosed)
|
||||
self.conn.sendClose(channel)
|
||||
self.conn.ssh_CHANNEL_CLOSE(b'\x00\x00\x00\x00')
|
||||
self.assertTrue(channel.gotOneClose)
|
||||
self.assertTrue(channel.gotClosed)
|
||||
|
||||
def test_CHANNEL_REQUEST_success(self):
|
||||
"""
|
||||
Test that channel requests that succeed send MSG_CHANNEL_SUCCESS.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_REQUEST(b'\x00\x00\x00\x00' + common.NS(b'test')
|
||||
+ b'\x00')
|
||||
self.assertEqual(channel.numberRequests, 1)
|
||||
d = self.conn.ssh_CHANNEL_REQUEST(b'\x00\x00\x00\x00' + common.NS(
|
||||
b'test') + b'\xff' + b'data')
|
||||
def check(result):
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_SUCCESS, b'\x00\x00\x00\xff')])
|
||||
d.addCallback(check)
|
||||
return d
|
||||
|
||||
def test_CHANNEL_REQUEST_failure(self):
|
||||
"""
|
||||
Test that channel requests that fail send MSG_CHANNEL_FAILURE.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
d = self.conn.ssh_CHANNEL_REQUEST(b'\x00\x00\x00\x00' + common.NS(
|
||||
b'test') + b'\xff')
|
||||
def check(result):
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_FAILURE, b'\x00\x00\x00\xff'
|
||||
)])
|
||||
d.addCallback(self.fail)
|
||||
d.addErrback(check)
|
||||
return d
|
||||
|
||||
def test_CHANNEL_REQUEST_SUCCESS(self):
|
||||
"""
|
||||
Test that channel request success messages cause the Deferred to be
|
||||
called back.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
d = self.conn.sendRequest(channel, b'test', b'data', True)
|
||||
self.conn.ssh_CHANNEL_SUCCESS(b'\x00\x00\x00\x00')
|
||||
def check(result):
|
||||
self.assertTrue(result)
|
||||
return d
|
||||
|
||||
def test_CHANNEL_REQUEST_FAILURE(self):
|
||||
"""
|
||||
Test that channel request failure messages cause the Deferred to be
|
||||
erred back.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
d = self.conn.sendRequest(channel, b'test', b'', True)
|
||||
self.conn.ssh_CHANNEL_FAILURE(b'\x00\x00\x00\x00')
|
||||
def check(result):
|
||||
self.assertEqual(result.value.value, 'channel request failed')
|
||||
d.addCallback(self.fail)
|
||||
d.addErrback(check)
|
||||
return d
|
||||
|
||||
def test_sendGlobalRequest(self):
|
||||
"""
|
||||
Test that global request messages are sent in the right format.
|
||||
"""
|
||||
d = self.conn.sendGlobalRequest(b'wantReply', b'data', True)
|
||||
# must be added to prevent errbacking during teardown
|
||||
d.addErrback(lambda failure: None)
|
||||
self.conn.sendGlobalRequest(b'noReply', b'', False)
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_GLOBAL_REQUEST, common.NS(b'wantReply') +
|
||||
b'\xffdata'),
|
||||
(connection.MSG_GLOBAL_REQUEST, common.NS(b'noReply') +
|
||||
b'\x00')])
|
||||
self.assertEqual(self.conn.deferreds, {'global':[d]})
|
||||
|
||||
def test_openChannel(self):
|
||||
"""
|
||||
Test that open channel messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self.conn.openChannel(channel, b'aaaa')
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_OPEN, common.NS(b'TestChannel') +
|
||||
b'\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x80\x00aaaa')])
|
||||
self.assertEqual(channel.id, 0)
|
||||
self.assertEqual(self.conn.localChannelID, 1)
|
||||
|
||||
def test_sendRequest(self):
|
||||
"""
|
||||
Test that channel request messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
d = self.conn.sendRequest(channel, b'test', b'test', True)
|
||||
# needed to prevent errbacks during teardown.
|
||||
d.addErrback(lambda failure: None)
|
||||
self.conn.sendRequest(channel, b'test2', b'', False)
|
||||
channel.localClosed = True # emulate sending a close message
|
||||
self.conn.sendRequest(channel, b'test3', b'', True)
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_REQUEST, b'\x00\x00\x00\xff' +
|
||||
common.NS(b'test') + b'\x01test'),
|
||||
(connection.MSG_CHANNEL_REQUEST, b'\x00\x00\x00\xff' +
|
||||
common.NS(b'test2') + b'\x00')])
|
||||
self.assertEqual(self.conn.deferreds[0], [d])
|
||||
|
||||
def test_adjustWindow(self):
|
||||
"""
|
||||
Test that channel window adjust messages cause bytes to be added
|
||||
to the window.
|
||||
"""
|
||||
channel = TestChannel(localWindow=5)
|
||||
self._openChannel(channel)
|
||||
channel.localWindowLeft = 0
|
||||
self.conn.adjustWindow(channel, 1)
|
||||
self.assertEqual(channel.localWindowLeft, 1)
|
||||
channel.localClosed = True
|
||||
self.conn.adjustWindow(channel, 2)
|
||||
self.assertEqual(channel.localWindowLeft, 1)
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_WINDOW_ADJUST, b'\x00\x00\x00\xff'
|
||||
b'\x00\x00\x00\x01')])
|
||||
|
||||
def test_sendData(self):
|
||||
"""
|
||||
Test that channel data messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.sendData(channel, b'a')
|
||||
channel.localClosed = True
|
||||
self.conn.sendData(channel, b'b')
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_DATA, b'\x00\x00\x00\xff' +
|
||||
common.NS(b'a'))])
|
||||
|
||||
def test_sendExtendedData(self):
|
||||
"""
|
||||
Test that channel extended data messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.sendExtendedData(channel, 1, b'test')
|
||||
channel.localClosed = True
|
||||
self.conn.sendExtendedData(channel, 2, b'test2')
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_EXTENDED_DATA, b'\x00\x00\x00\xff' +
|
||||
b'\x00\x00\x00\x01' + common.NS(b'test'))])
|
||||
|
||||
def test_sendEOF(self):
|
||||
"""
|
||||
Test that channel EOF messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.sendEOF(channel)
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_EOF, b'\x00\x00\x00\xff')])
|
||||
channel.localClosed = True
|
||||
self.conn.sendEOF(channel)
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_EOF, b'\x00\x00\x00\xff')])
|
||||
|
||||
def test_sendClose(self):
|
||||
"""
|
||||
Test that channel close messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.sendClose(channel)
|
||||
self.assertTrue(channel.localClosed)
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b'\x00\x00\x00\xff')])
|
||||
self.conn.sendClose(channel)
|
||||
self.assertEqual(self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b'\x00\x00\x00\xff')])
|
||||
|
||||
channel2 = TestChannel()
|
||||
self._openChannel(channel2)
|
||||
self.assertTrue(channel2.gotOpen)
|
||||
self.assertFalse(channel2.gotClosed)
|
||||
channel2.remoteClosed = True
|
||||
self.conn.sendClose(channel2)
|
||||
self.assertTrue(channel2.gotClosed)
|
||||
|
||||
def test_getChannelWithAvatar(self):
|
||||
"""
|
||||
Test that getChannel dispatches to the avatar when an avatar is
|
||||
present. Correct functioning without the avatar is verified in
|
||||
test_CHANNEL_OPEN.
|
||||
"""
|
||||
channel = self.conn.getChannel(b'TestChannel', 50, 30, b'data')
|
||||
self.assertEqual(channel.data, b'data')
|
||||
self.assertEqual(channel.remoteWindowLeft, 50)
|
||||
self.assertEqual(channel.remoteMaxPacket, 30)
|
||||
self.assertRaises(error.ConchError, self.conn.getChannel,
|
||||
b'BadChannel', 50, 30, b'data')
|
||||
|
||||
def test_gotGlobalRequestWithoutAvatar(self):
|
||||
"""
|
||||
Test that gotGlobalRequests dispatches to global_* without an avatar.
|
||||
"""
|
||||
del self.transport.avatar
|
||||
self.assertTrue(self.conn.gotGlobalRequest(b'TestGlobal', b'data'))
|
||||
self.assertEqual(self.conn.gotGlobalRequest(b'Test-Data', b'data'),
|
||||
(True, b'data'))
|
||||
self.assertFalse(self.conn.gotGlobalRequest(b'BadGlobal', b'data'))
|
||||
|
||||
|
||||
def test_channelClosedCausesLeftoverChannelDeferredsToErrback(self):
|
||||
"""
|
||||
Whenever an SSH channel gets closed any Deferred that was returned by a
|
||||
sendRequest() on its parent connection must be errbacked.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
|
||||
d = self.conn.sendRequest(
|
||||
channel, b"dummyrequest", b"dummydata", wantReply=1)
|
||||
d = self.assertFailure(d, error.ConchError)
|
||||
self.conn.channelClosed(channel)
|
||||
return d
|
||||
|
||||
|
||||
|
||||
class CleanConnectionShutdownTests(unittest.TestCase):
|
||||
"""
|
||||
Check whether correct cleanup is performed on connection shutdown.
|
||||
"""
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
if test_userauth.transport is None:
|
||||
skip = "Cannot run without both cryptography and pyasn1"
|
||||
|
||||
def setUp(self):
|
||||
self.transport = test_userauth.FakeTransport(None)
|
||||
self.transport.avatar = TestAvatar()
|
||||
self.conn = TestConnection()
|
||||
self.conn.transport = self.transport
|
||||
|
||||
|
||||
def test_serviceStoppedCausesLeftoverGlobalDeferredsToErrback(self):
|
||||
"""
|
||||
Once the service is stopped any leftover global deferred returned by
|
||||
a sendGlobalRequest() call must be errbacked.
|
||||
"""
|
||||
self.conn.serviceStarted()
|
||||
|
||||
d = self.conn.sendGlobalRequest(
|
||||
b"dummyrequest", b"dummydata", wantReply=1)
|
||||
d = self.assertFailure(d, error.ConchError)
|
||||
self.conn.serviceStopped()
|
||||
return d
|
||||
@@ -0,0 +1,333 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.client.default}.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
import sys
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
if requireModule('cryptography') and requireModule('pyasn1'):
|
||||
from twisted.conch.client.agent import SSHAgentClient
|
||||
from twisted.conch.client.default import SSHUserAuthClient
|
||||
from twisted.conch.client.options import ConchOptions
|
||||
from twisted.conch.client import default
|
||||
from twisted.conch.ssh.keys import Key
|
||||
skip = None
|
||||
else:
|
||||
skip = "cryptography and PyASN1 required for twisted.conch.client.default."
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.conch.error import ConchError
|
||||
from twisted.conch.test import keydata
|
||||
from twisted.test.proto_helpers import StringTransport
|
||||
from twisted.python.compat import nativeString
|
||||
from twisted.python.runtime import platform
|
||||
|
||||
if platform.isWindows():
|
||||
windowsSkip = (
|
||||
"genericAnswers and getPassword does not work on Windows."
|
||||
" Should be fixed as part of fixing bug 6409 and 6410")
|
||||
else:
|
||||
windowsSkip = skip
|
||||
|
||||
ttySkip = None
|
||||
if not sys.stdin.isatty():
|
||||
ttySkip = "sys.stdin is not an interactive tty"
|
||||
if not sys.stdout.isatty():
|
||||
ttySkip = "sys.stdout is not an interactive tty"
|
||||
|
||||
|
||||
|
||||
class SSHUserAuthClientTests(TestCase):
|
||||
"""
|
||||
Tests for L{SSHUserAuthClient}.
|
||||
|
||||
@type rsaPublic: L{Key}
|
||||
@ivar rsaPublic: A public RSA key.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.rsaPublic = Key.fromString(keydata.publicRSA_openssh)
|
||||
self.tmpdir = FilePath(self.mktemp())
|
||||
self.tmpdir.makedirs()
|
||||
self.rsaFile = self.tmpdir.child('id_rsa')
|
||||
self.rsaFile.setContent(keydata.privateRSA_openssh)
|
||||
self.tmpdir.child('id_rsa.pub').setContent(keydata.publicRSA_openssh)
|
||||
|
||||
|
||||
def test_signDataWithAgent(self):
|
||||
"""
|
||||
When connected to an agent, L{SSHUserAuthClient} can use it to
|
||||
request signatures of particular data with a particular L{Key}.
|
||||
"""
|
||||
client = SSHUserAuthClient(b"user", ConchOptions(), None)
|
||||
agent = SSHAgentClient()
|
||||
transport = StringTransport()
|
||||
agent.makeConnection(transport)
|
||||
client.keyAgent = agent
|
||||
cleartext = b"Sign here"
|
||||
client.signData(self.rsaPublic, cleartext)
|
||||
self.assertEqual(
|
||||
transport.value(),
|
||||
b"\x00\x00\x01\x2d\r\x00\x00\x01\x17" + self.rsaPublic.blob() +
|
||||
b"\x00\x00\x00\t" + cleartext +
|
||||
b"\x00\x00\x00\x00")
|
||||
|
||||
|
||||
def test_agentGetPublicKey(self):
|
||||
"""
|
||||
L{SSHUserAuthClient} looks up public keys from the agent using the
|
||||
L{SSHAgentClient} class. That L{SSHAgentClient.getPublicKey} returns a
|
||||
L{Key} object with one of the public keys in the agent. If no more
|
||||
keys are present, it returns L{None}.
|
||||
"""
|
||||
agent = SSHAgentClient()
|
||||
agent.blobs = [self.rsaPublic.blob()]
|
||||
key = agent.getPublicKey()
|
||||
self.assertTrue(key.isPublic())
|
||||
self.assertEqual(key, self.rsaPublic)
|
||||
self.assertIsNone(agent.getPublicKey())
|
||||
|
||||
|
||||
def test_getPublicKeyFromFile(self):
|
||||
"""
|
||||
L{SSHUserAuthClient.getPublicKey()} is able to get a public key from
|
||||
the first file described by its options' C{identitys} list, and return
|
||||
the corresponding public L{Key} object.
|
||||
"""
|
||||
options = ConchOptions()
|
||||
options.identitys = [self.rsaFile.path]
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
key = client.getPublicKey()
|
||||
self.assertTrue(key.isPublic())
|
||||
self.assertEqual(key, self.rsaPublic)
|
||||
|
||||
|
||||
def test_getPublicKeyAgentFallback(self):
|
||||
"""
|
||||
If an agent is present, but doesn't return a key,
|
||||
L{SSHUserAuthClient.getPublicKey} continue with the normal key lookup.
|
||||
"""
|
||||
options = ConchOptions()
|
||||
options.identitys = [self.rsaFile.path]
|
||||
agent = SSHAgentClient()
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
client.keyAgent = agent
|
||||
key = client.getPublicKey()
|
||||
self.assertTrue(key.isPublic())
|
||||
self.assertEqual(key, self.rsaPublic)
|
||||
|
||||
|
||||
def test_getPublicKeyBadKeyError(self):
|
||||
"""
|
||||
If L{keys.Key.fromFile} raises a L{keys.BadKeyError}, the
|
||||
L{SSHUserAuthClient.getPublicKey} tries again to get a public key by
|
||||
calling itself recursively.
|
||||
"""
|
||||
options = ConchOptions()
|
||||
self.tmpdir.child('id_dsa.pub').setContent(keydata.publicDSA_openssh)
|
||||
dsaFile = self.tmpdir.child('id_dsa')
|
||||
dsaFile.setContent(keydata.privateDSA_openssh)
|
||||
options.identitys = [self.rsaFile.path, dsaFile.path]
|
||||
self.tmpdir.child('id_rsa.pub').setContent(b'not a key!')
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
key = client.getPublicKey()
|
||||
self.assertTrue(key.isPublic())
|
||||
self.assertEqual(key, Key.fromString(keydata.publicDSA_openssh))
|
||||
self.assertEqual(client.usedFiles, [self.rsaFile.path, dsaFile.path])
|
||||
|
||||
|
||||
def test_getPrivateKey(self):
|
||||
"""
|
||||
L{SSHUserAuthClient.getPrivateKey} will load a private key from the
|
||||
last used file populated by L{SSHUserAuthClient.getPublicKey}, and
|
||||
return a L{Deferred} which fires with the corresponding private L{Key}.
|
||||
"""
|
||||
rsaPrivate = Key.fromString(keydata.privateRSA_openssh)
|
||||
options = ConchOptions()
|
||||
options.identitys = [self.rsaFile.path]
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
# Populate the list of used files
|
||||
client.getPublicKey()
|
||||
|
||||
def _cbGetPrivateKey(key):
|
||||
self.assertFalse(key.isPublic())
|
||||
self.assertEqual(key, rsaPrivate)
|
||||
|
||||
return client.getPrivateKey().addCallback(_cbGetPrivateKey)
|
||||
|
||||
|
||||
def test_getPrivateKeyPassphrase(self):
|
||||
"""
|
||||
L{SSHUserAuthClient} can get a private key from a file, and return a
|
||||
Deferred called back with a private L{Key} object, even if the key is
|
||||
encrypted.
|
||||
"""
|
||||
rsaPrivate = Key.fromString(keydata.privateRSA_openssh)
|
||||
passphrase = b'this is the passphrase'
|
||||
self.rsaFile.setContent(rsaPrivate.toString('openssh', passphrase))
|
||||
options = ConchOptions()
|
||||
options.identitys = [self.rsaFile.path]
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
# Populate the list of used files
|
||||
client.getPublicKey()
|
||||
|
||||
def _getPassword(prompt):
|
||||
self.assertEqual(
|
||||
prompt,
|
||||
"Enter passphrase for key '%s': " % (self.rsaFile.path,))
|
||||
return nativeString(passphrase)
|
||||
|
||||
def _cbGetPrivateKey(key):
|
||||
self.assertFalse(key.isPublic())
|
||||
self.assertEqual(key, rsaPrivate)
|
||||
|
||||
self.patch(client, '_getPassword', _getPassword)
|
||||
return client.getPrivateKey().addCallback(_cbGetPrivateKey)
|
||||
|
||||
|
||||
def test_getPassword(self):
|
||||
"""
|
||||
Get the password using
|
||||
L{twisted.conch.client.default.SSHUserAuthClient.getPassword}
|
||||
"""
|
||||
class FakeTransport:
|
||||
def __init__(self, host):
|
||||
self.transport = self
|
||||
self.host = host
|
||||
def getPeer(self):
|
||||
return self
|
||||
|
||||
options = ConchOptions()
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
client.transport = FakeTransport("127.0.0.1")
|
||||
|
||||
def getpass(prompt):
|
||||
self.assertEqual(prompt, "user@127.0.0.1's password: ")
|
||||
return 'bad password'
|
||||
|
||||
self.patch(default.getpass, 'getpass', getpass)
|
||||
d = client.getPassword()
|
||||
d.addCallback(self.assertEqual, b'bad password')
|
||||
return d
|
||||
|
||||
test_getPassword.skip = windowsSkip or ttySkip
|
||||
|
||||
|
||||
def test_getPasswordPrompt(self):
|
||||
"""
|
||||
Get the password using
|
||||
L{twisted.conch.client.default.SSHUserAuthClient.getPassword}
|
||||
using a different prompt.
|
||||
"""
|
||||
options = ConchOptions()
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
prompt = b"Give up your password"
|
||||
|
||||
def getpass(p):
|
||||
self.assertEqual(p, nativeString(prompt))
|
||||
return 'bad password'
|
||||
|
||||
self.patch(default.getpass, 'getpass', getpass)
|
||||
d = client.getPassword(prompt)
|
||||
d.addCallback(self.assertEqual, b'bad password')
|
||||
return d
|
||||
|
||||
test_getPasswordPrompt.skip = windowsSkip or ttySkip
|
||||
|
||||
|
||||
def test_getPasswordConchError(self):
|
||||
"""
|
||||
Get the password using
|
||||
L{twisted.conch.client.default.SSHUserAuthClient.getPassword}
|
||||
and trigger a {twisted.conch.error import ConchError}.
|
||||
"""
|
||||
options = ConchOptions()
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
|
||||
def getpass(prompt):
|
||||
raise KeyboardInterrupt("User pressed CTRL-C")
|
||||
|
||||
self.patch(default.getpass, 'getpass', getpass)
|
||||
stdout, stdin = sys.stdout, sys.stdin
|
||||
d = client.getPassword(b'?')
|
||||
@d.addErrback
|
||||
def check_sys(fail):
|
||||
self.assertEqual(
|
||||
[stdout, stdin], [sys.stdout, sys.stdin])
|
||||
return fail
|
||||
self.assertFailure(d, ConchError)
|
||||
|
||||
test_getPasswordConchError.skip = windowsSkip or ttySkip
|
||||
|
||||
|
||||
def test_getGenericAnswers(self):
|
||||
"""
|
||||
L{twisted.conch.client.default.SSHUserAuthClient.getGenericAnswers}
|
||||
"""
|
||||
options = ConchOptions()
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
|
||||
def getpass(prompt):
|
||||
self.assertEqual(prompt, "pass prompt")
|
||||
return "getpass"
|
||||
|
||||
self.patch(default.getpass, 'getpass', getpass)
|
||||
|
||||
def raw_input(prompt):
|
||||
self.assertEqual(prompt, "raw_input prompt")
|
||||
return "raw_input"
|
||||
|
||||
self.patch(default, 'raw_input', raw_input)
|
||||
d = client.getGenericAnswers(
|
||||
b"Name", b"Instruction", [
|
||||
(b"pass prompt", False), (b"raw_input prompt", True)])
|
||||
d.addCallback(
|
||||
self.assertListEqual, ["getpass", "raw_input"])
|
||||
return d
|
||||
|
||||
test_getGenericAnswers.skip = windowsSkip or ttySkip
|
||||
|
||||
|
||||
|
||||
|
||||
class ConchOptionsParsing(TestCase):
|
||||
"""
|
||||
Options parsing.
|
||||
"""
|
||||
def test_macs(self):
|
||||
"""
|
||||
Specify MAC algorithms.
|
||||
"""
|
||||
opts = ConchOptions()
|
||||
e = self.assertRaises(SystemExit, opts.opt_macs, "invalid-mac")
|
||||
self.assertIn("Unknown mac type", e.code)
|
||||
opts = ConchOptions()
|
||||
opts.opt_macs("hmac-sha2-512")
|
||||
self.assertEqual(opts['macs'], [b"hmac-sha2-512"])
|
||||
opts.opt_macs(b"hmac-sha2-512")
|
||||
self.assertEqual(opts['macs'], [b"hmac-sha2-512"])
|
||||
opts.opt_macs("hmac-sha2-256,hmac-sha1,hmac-md5")
|
||||
self.assertEqual(opts['macs'], [b"hmac-sha2-256", b"hmac-sha1", b"hmac-md5"])
|
||||
|
||||
|
||||
def test_host_key_algorithms(self):
|
||||
"""
|
||||
Specify host key algorithms.
|
||||
"""
|
||||
opts = ConchOptions()
|
||||
e = self.assertRaises(SystemExit, opts.opt_host_key_algorithms, "invalid-key")
|
||||
self.assertIn("Unknown host key type", e.code)
|
||||
opts = ConchOptions()
|
||||
opts.opt_host_key_algorithms("ssh-rsa")
|
||||
self.assertEqual(opts['host-key-algorithms'], [b"ssh-rsa"])
|
||||
opts.opt_host_key_algorithms(b"ssh-dss")
|
||||
self.assertEqual(opts['host-key-algorithms'], [b"ssh-dss"])
|
||||
opts.opt_host_key_algorithms("ssh-rsa,ssh-dss")
|
||||
self.assertEqual(opts['host-key-algorithms'], [b"ssh-rsa", b"ssh-dss"])
|
||||
@@ -0,0 +1,63 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.ssh.forwarding}.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
if cryptography:
|
||||
from twisted.conch.ssh import forwarding
|
||||
|
||||
from twisted.internet.address import IPv6Address
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet.test.test_endpoints import deterministicResolvingReactor
|
||||
from twisted.test.proto_helpers import MemoryReactorClock, StringTransport
|
||||
|
||||
|
||||
class TestSSHConnectForwardingChannel(unittest.TestCase):
|
||||
"""
|
||||
Unit and integration tests for L{SSHConnectForwardingChannel}.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
def makeTCPConnection(self, reactor):
|
||||
"""
|
||||
Fake that connection was established for first connectTCP request made
|
||||
on C{reactor}.
|
||||
|
||||
@param reactor: Reactor on which to fake the connection.
|
||||
@type reactor: A reactor.
|
||||
"""
|
||||
factory = reactor.tcpClients[0][2]
|
||||
connector = reactor.connectors[0]
|
||||
protocol = factory.buildProtocol(None)
|
||||
transport = StringTransport(peerAddress=connector.getDestination())
|
||||
protocol.makeConnection(transport)
|
||||
|
||||
|
||||
def test_channelOpenHostnameRequests(self):
|
||||
"""
|
||||
When a hostname is sent as part of forwarding requests, it
|
||||
is resolved using HostnameEndpoint's resolver.
|
||||
"""
|
||||
sut = forwarding.SSHConnectForwardingChannel(
|
||||
hostport=('fwd.example.org', 1234))
|
||||
# Patch channel and resolver to not touch the network.
|
||||
memoryReactor = MemoryReactorClock()
|
||||
sut._reactor = deterministicResolvingReactor(memoryReactor, ['::1'])
|
||||
sut.channelOpen(None)
|
||||
|
||||
self.makeTCPConnection(memoryReactor)
|
||||
self.successResultOf(sut._channelOpenDeferred)
|
||||
# Channel is connected using a forwarding client to the resolved
|
||||
# address of the requested host.
|
||||
self.assertIsInstance(sut.client, forwarding.SSHForwardingClient)
|
||||
self.assertEqual(
|
||||
IPv6Address('TCP', '::1', 1234), sut.client.transport.getPeer())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the command-line interfaces to conch.
|
||||
"""
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
if requireModule('pyasn1'):
|
||||
pyasn1Skip = None
|
||||
else:
|
||||
pyasn1Skip = "Cannot run without PyASN1"
|
||||
|
||||
if requireModule('cryptography'):
|
||||
cryptoSkip = None
|
||||
else:
|
||||
cryptoSkip = "can't run w/o cryptography"
|
||||
|
||||
if requireModule('tty'):
|
||||
ttySkip = None
|
||||
else:
|
||||
ttySkip = "can't run w/o tty"
|
||||
|
||||
try:
|
||||
import Tkinter
|
||||
except ImportError:
|
||||
tkskip = "can't run w/o Tkinter"
|
||||
else:
|
||||
try:
|
||||
Tkinter.Tk().destroy()
|
||||
except Tkinter.TclError as e:
|
||||
tkskip = "Can't test Tkinter: " + str(e)
|
||||
else:
|
||||
tkskip = None
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.scripts.test.test_scripts import ScriptTestsMixin
|
||||
from twisted.python.test.test_shellcomp import ZshScriptTestMixin
|
||||
|
||||
|
||||
|
||||
class ScriptTests(TestCase, ScriptTestsMixin):
|
||||
"""
|
||||
Tests for the Conch scripts.
|
||||
"""
|
||||
skip = pyasn1Skip or cryptoSkip
|
||||
|
||||
|
||||
def test_conch(self):
|
||||
self.scriptTest("conch/conch")
|
||||
test_conch.skip = ttySkip or skip
|
||||
|
||||
|
||||
def test_cftp(self):
|
||||
self.scriptTest("conch/cftp")
|
||||
test_cftp.skip = ttySkip or skip
|
||||
|
||||
|
||||
def test_ckeygen(self):
|
||||
self.scriptTest("conch/ckeygen")
|
||||
|
||||
|
||||
def test_tkconch(self):
|
||||
self.scriptTest("conch/tkconch")
|
||||
test_tkconch.skip = tkskip or skip
|
||||
|
||||
|
||||
|
||||
class ZshIntegrationTests(TestCase, ZshScriptTestMixin):
|
||||
"""
|
||||
Test that zsh completion functions are generated without error
|
||||
"""
|
||||
generateFor = [('conch', 'twisted.conch.scripts.conch.ClientOptions'),
|
||||
('cftp', 'twisted.conch.scripts.cftp.ClientOptions'),
|
||||
('ckeygen', 'twisted.conch.scripts.ckeygen.GeneralOptions'),
|
||||
('tkconch', 'twisted.conch.scripts.tkconch.GeneralOptions'),
|
||||
]
|
||||
@@ -0,0 +1,997 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.ssh}.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
pyasn1 = requireModule("pyasn1")
|
||||
|
||||
if cryptography:
|
||||
from twisted.conch.ssh import common, forwarding, session, _kex
|
||||
from twisted.conch import avatar, error
|
||||
else:
|
||||
class avatar:
|
||||
class ConchUser: pass
|
||||
|
||||
from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh
|
||||
from twisted.conch.test.keydata import publicDSA_openssh, privateDSA_openssh
|
||||
from twisted.cred import portal
|
||||
from twisted.cred.error import UnauthorizedLogin
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
from twisted.internet.error import ProcessTerminated
|
||||
from twisted.python import failure, log
|
||||
from twisted.trial import unittest
|
||||
|
||||
from twisted.conch.test.loopback import LoopbackRelay
|
||||
|
||||
|
||||
|
||||
class ConchTestRealm(object):
|
||||
"""
|
||||
A realm which expects a particular avatarId to log in once and creates a
|
||||
L{ConchTestAvatar} for that request.
|
||||
|
||||
@ivar expectedAvatarID: The only avatarID that this realm will produce an
|
||||
avatar for.
|
||||
|
||||
@ivar avatar: A reference to the avatar after it is requested.
|
||||
"""
|
||||
avatar = None
|
||||
|
||||
def __init__(self, expectedAvatarID):
|
||||
self.expectedAvatarID = expectedAvatarID
|
||||
|
||||
|
||||
def requestAvatar(self, avatarID, mind, *interfaces):
|
||||
"""
|
||||
Return a new L{ConchTestAvatar} if the avatarID matches the expected one
|
||||
and this is the first avatar request.
|
||||
"""
|
||||
if avatarID == self.expectedAvatarID:
|
||||
if self.avatar is not None:
|
||||
raise UnauthorizedLogin("Only one login allowed")
|
||||
self.avatar = ConchTestAvatar()
|
||||
return interfaces[0], self.avatar, self.avatar.logout
|
||||
raise UnauthorizedLogin(
|
||||
"Only %r may log in, not %r" % (self.expectedAvatarID, avatarID))
|
||||
|
||||
|
||||
|
||||
class ConchTestAvatar(avatar.ConchUser):
|
||||
"""
|
||||
An avatar against which various SSH features can be tested.
|
||||
|
||||
@ivar loggedOut: A flag indicating whether the avatar logout method has been
|
||||
called.
|
||||
"""
|
||||
if not cryptography:
|
||||
skip = "cannot run without cryptography"
|
||||
|
||||
loggedOut = False
|
||||
|
||||
def __init__(self):
|
||||
avatar.ConchUser.__init__(self)
|
||||
self.listeners = {}
|
||||
self.globalRequests = {}
|
||||
self.channelLookup.update(
|
||||
{b'session': session.SSHSession,
|
||||
b'direct-tcpip':forwarding.openConnectForwardingClient})
|
||||
self.subsystemLookup.update({b'crazy': CrazySubsystem})
|
||||
|
||||
|
||||
def global_foo(self, data):
|
||||
self.globalRequests['foo'] = data
|
||||
return 1
|
||||
|
||||
|
||||
def global_foo_2(self, data):
|
||||
self.globalRequests['foo_2'] = data
|
||||
return 1, b'data'
|
||||
|
||||
|
||||
def global_tcpip_forward(self, data):
|
||||
host, port = forwarding.unpackGlobal_tcpip_forward(data)
|
||||
try:
|
||||
listener = reactor.listenTCP(
|
||||
port, forwarding.SSHListenForwardingFactory(
|
||||
self.conn, (host, port),
|
||||
forwarding.SSHListenServerForwardingChannel),
|
||||
interface=host)
|
||||
except:
|
||||
log.err(None, "something went wrong with remote->local forwarding")
|
||||
return 0
|
||||
else:
|
||||
self.listeners[(host, port)] = listener
|
||||
return 1
|
||||
|
||||
|
||||
def global_cancel_tcpip_forward(self, data):
|
||||
host, port = forwarding.unpackGlobal_tcpip_forward(data)
|
||||
listener = self.listeners.get((host, port), None)
|
||||
if not listener:
|
||||
return 0
|
||||
del self.listeners[(host, port)]
|
||||
listener.stopListening()
|
||||
return 1
|
||||
|
||||
|
||||
def logout(self):
|
||||
self.loggedOut = True
|
||||
for listener in self.listeners.values():
|
||||
log.msg('stopListening %s' % listener)
|
||||
listener.stopListening()
|
||||
|
||||
|
||||
|
||||
class ConchSessionForTestAvatar(object):
|
||||
"""
|
||||
An ISession adapter for ConchTestAvatar.
|
||||
"""
|
||||
def __init__(self, avatar):
|
||||
"""
|
||||
Initialize the session and create a reference to it on the avatar for
|
||||
later inspection.
|
||||
"""
|
||||
self.avatar = avatar
|
||||
self.avatar._testSession = self
|
||||
self.cmd = None
|
||||
self.proto = None
|
||||
self.ptyReq = False
|
||||
self.eof = 0
|
||||
self.onClose = defer.Deferred()
|
||||
|
||||
|
||||
def getPty(self, term, windowSize, attrs):
|
||||
log.msg('pty req')
|
||||
self._terminalType = term
|
||||
self._windowSize = windowSize
|
||||
self.ptyReq = True
|
||||
|
||||
|
||||
def openShell(self, proto):
|
||||
log.msg('opening shell')
|
||||
self.proto = proto
|
||||
EchoTransport(proto)
|
||||
self.cmd = b'shell'
|
||||
|
||||
|
||||
def execCommand(self, proto, cmd):
|
||||
self.cmd = cmd
|
||||
self.proto = proto
|
||||
f = cmd.split()[0]
|
||||
if f == b'false':
|
||||
t = FalseTransport(proto)
|
||||
# Avoid disconnecting this immediately. If the channel is closed
|
||||
# before execCommand even returns the caller gets confused.
|
||||
reactor.callLater(0, t.loseConnection)
|
||||
elif f == b'echo':
|
||||
t = EchoTransport(proto)
|
||||
t.write(cmd[5:])
|
||||
t.loseConnection()
|
||||
elif f == b'secho':
|
||||
t = SuperEchoTransport(proto)
|
||||
t.write(cmd[6:])
|
||||
t.loseConnection()
|
||||
elif f == b'eecho':
|
||||
t = ErrEchoTransport(proto)
|
||||
t.write(cmd[6:])
|
||||
t.loseConnection()
|
||||
else:
|
||||
raise error.ConchError('bad exec')
|
||||
self.avatar.conn.transport.expectedLoseConnection = 1
|
||||
|
||||
|
||||
def eofReceived(self):
|
||||
self.eof = 1
|
||||
|
||||
|
||||
def closed(self):
|
||||
log.msg('closed cmd "%s"' % self.cmd)
|
||||
self.remoteWindowLeftAtClose = self.proto.session.remoteWindowLeft
|
||||
self.onClose.callback(None)
|
||||
|
||||
from twisted.python import components
|
||||
|
||||
if cryptography:
|
||||
components.registerAdapter(ConchSessionForTestAvatar, ConchTestAvatar,
|
||||
session.ISession)
|
||||
|
||||
class CrazySubsystem(protocol.Protocol):
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
pass
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
good ... good
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class FalseTransport:
|
||||
"""
|
||||
False transport should act like a /bin/false execution, i.e. just exit with
|
||||
nonzero status, writing nothing to the terminal.
|
||||
|
||||
@ivar proto: The protocol associated with this transport.
|
||||
@ivar closed: A flag tracking whether C{loseConnection} has been called yet.
|
||||
"""
|
||||
|
||||
def __init__(self, p):
|
||||
"""
|
||||
@type p L{twisted.conch.ssh.session.SSHSessionProcessProtocol} instance
|
||||
"""
|
||||
self.proto = p
|
||||
p.makeConnection(self)
|
||||
self.closed = 0
|
||||
|
||||
|
||||
def loseConnection(self):
|
||||
"""
|
||||
Disconnect the protocol associated with this transport.
|
||||
"""
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = 1
|
||||
self.proto.inConnectionLost()
|
||||
self.proto.outConnectionLost()
|
||||
self.proto.errConnectionLost()
|
||||
self.proto.processEnded(failure.Failure(ProcessTerminated(255, None, None)))
|
||||
|
||||
|
||||
|
||||
class EchoTransport:
|
||||
|
||||
def __init__(self, p):
|
||||
self.proto = p
|
||||
p.makeConnection(self)
|
||||
self.closed = 0
|
||||
|
||||
def write(self, data):
|
||||
log.msg(repr(data))
|
||||
self.proto.outReceived(data)
|
||||
self.proto.outReceived(b'\r\n')
|
||||
if b'\x00' in data: # mimic 'exit' for the shell test
|
||||
self.loseConnection()
|
||||
|
||||
def loseConnection(self):
|
||||
if self.closed: return
|
||||
self.closed = 1
|
||||
self.proto.inConnectionLost()
|
||||
self.proto.outConnectionLost()
|
||||
self.proto.errConnectionLost()
|
||||
self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
|
||||
|
||||
class ErrEchoTransport:
|
||||
|
||||
def __init__(self, p):
|
||||
self.proto = p
|
||||
p.makeConnection(self)
|
||||
self.closed = 0
|
||||
|
||||
def write(self, data):
|
||||
self.proto.errReceived(data)
|
||||
self.proto.errReceived(b'\r\n')
|
||||
|
||||
def loseConnection(self):
|
||||
if self.closed: return
|
||||
self.closed = 1
|
||||
self.proto.inConnectionLost()
|
||||
self.proto.outConnectionLost()
|
||||
self.proto.errConnectionLost()
|
||||
self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
|
||||
|
||||
class SuperEchoTransport:
|
||||
|
||||
def __init__(self, p):
|
||||
self.proto = p
|
||||
p.makeConnection(self)
|
||||
self.closed = 0
|
||||
|
||||
def write(self, data):
|
||||
self.proto.outReceived(data)
|
||||
self.proto.outReceived(b'\r\n')
|
||||
self.proto.errReceived(data)
|
||||
self.proto.errReceived(b'\r\n')
|
||||
|
||||
def loseConnection(self):
|
||||
if self.closed: return
|
||||
self.closed = 1
|
||||
self.proto.inConnectionLost()
|
||||
self.proto.outConnectionLost()
|
||||
self.proto.errConnectionLost()
|
||||
self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
|
||||
|
||||
|
||||
if cryptography is not None and pyasn1 is not None:
|
||||
from twisted.conch import checkers
|
||||
from twisted.conch.ssh import channel, connection, factory, keys
|
||||
from twisted.conch.ssh import transport, userauth
|
||||
|
||||
class ConchTestPasswordChecker:
|
||||
credentialInterfaces = checkers.IUsernamePassword,
|
||||
|
||||
def requestAvatarId(self, credentials):
|
||||
if credentials.username == b'testuser' and credentials.password == b'testpass':
|
||||
return defer.succeed(credentials.username)
|
||||
return defer.fail(Exception("Bad credentials"))
|
||||
|
||||
|
||||
class ConchTestSSHChecker(checkers.SSHProtocolChecker):
|
||||
|
||||
def areDone(self, avatarId):
|
||||
if avatarId != b'testuser' or len(self.successfulCredentials[avatarId]) < 2:
|
||||
return False
|
||||
return True
|
||||
|
||||
class ConchTestServerFactory(factory.SSHFactory):
|
||||
noisy = 0
|
||||
|
||||
services = {
|
||||
b'ssh-userauth':userauth.SSHUserAuthServer,
|
||||
b'ssh-connection':connection.SSHConnection
|
||||
}
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
proto = ConchTestServer()
|
||||
proto.supportedPublicKeys = self.privateKeys.keys()
|
||||
proto.factory = self
|
||||
|
||||
if hasattr(self, 'expectedLoseConnection'):
|
||||
proto.expectedLoseConnection = self.expectedLoseConnection
|
||||
|
||||
self.proto = proto
|
||||
return proto
|
||||
|
||||
def getPublicKeys(self):
|
||||
return {
|
||||
b'ssh-rsa': keys.Key.fromString(publicRSA_openssh),
|
||||
b'ssh-dss': keys.Key.fromString(publicDSA_openssh)
|
||||
}
|
||||
|
||||
def getPrivateKeys(self):
|
||||
return {
|
||||
b'ssh-rsa': keys.Key.fromString(privateRSA_openssh),
|
||||
b'ssh-dss': keys.Key.fromString(privateDSA_openssh)
|
||||
}
|
||||
|
||||
def getPrimes(self):
|
||||
"""
|
||||
Diffie-Hellman primes that can be used for the
|
||||
diffie-hellman-group-exchange-sha1 key exchange.
|
||||
|
||||
@return: The primes and generators.
|
||||
@rtype: L{dict} mapping the key size to a C{list} of
|
||||
C{(generator, prime)} tupple.
|
||||
"""
|
||||
# In these tests, we hardwire the prime values to those defined by
|
||||
# the diffie-hellman-group14-sha1 key exchange algorithm, to avoid
|
||||
# requiring a moduli file when running tests.
|
||||
# See OpenSSHFactory.getPrimes.
|
||||
return {
|
||||
2048: [
|
||||
_kex.getDHGeneratorAndPrime(
|
||||
b'diffie-hellman-group14-sha1')]
|
||||
}
|
||||
|
||||
def getService(self, trans, name):
|
||||
return factory.SSHFactory.getService(self, trans, name)
|
||||
|
||||
class ConchTestBase:
|
||||
|
||||
done = 0
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.done:
|
||||
return
|
||||
if not hasattr(self, 'expectedLoseConnection'):
|
||||
raise unittest.FailTest(
|
||||
'unexpectedly lost connection %s\n%s' % (self, reason))
|
||||
self.done = 1
|
||||
|
||||
def receiveError(self, reasonCode, desc):
|
||||
self.expectedLoseConnection = 1
|
||||
# Some versions of OpenSSH (for example, OpenSSH_5.3p1) will
|
||||
# send a DISCONNECT_BY_APPLICATION error before closing the
|
||||
# connection. Other, older versions (for example,
|
||||
# OpenSSH_5.1p1), won't. So accept this particular error here,
|
||||
# but no others.
|
||||
if reasonCode != transport.DISCONNECT_BY_APPLICATION:
|
||||
log.err(
|
||||
Exception(
|
||||
'got disconnect for %s: reason %s, desc: %s' % (
|
||||
self, reasonCode, desc)))
|
||||
self.loseConnection()
|
||||
|
||||
def receiveUnimplemented(self, seqID):
|
||||
raise unittest.FailTest('got unimplemented: seqid %s' % (seqID,))
|
||||
self.expectedLoseConnection = 1
|
||||
self.loseConnection()
|
||||
|
||||
class ConchTestServer(ConchTestBase, transport.SSHServerTransport):
|
||||
|
||||
def connectionLost(self, reason):
|
||||
ConchTestBase.connectionLost(self, reason)
|
||||
transport.SSHServerTransport.connectionLost(self, reason)
|
||||
|
||||
|
||||
class ConchTestClient(ConchTestBase, transport.SSHClientTransport):
|
||||
"""
|
||||
@ivar _channelFactory: A callable which accepts an SSH connection and
|
||||
returns a channel which will be attached to a new channel on that
|
||||
connection.
|
||||
"""
|
||||
def __init__(self, channelFactory):
|
||||
self._channelFactory = channelFactory
|
||||
|
||||
def connectionLost(self, reason):
|
||||
ConchTestBase.connectionLost(self, reason)
|
||||
transport.SSHClientTransport.connectionLost(self, reason)
|
||||
|
||||
def verifyHostKey(self, key, fp):
|
||||
keyMatch = key == keys.Key.fromString(publicRSA_openssh).blob()
|
||||
fingerprintMatch = (
|
||||
fp == b'85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da')
|
||||
if keyMatch and fingerprintMatch:
|
||||
return defer.succeed(1)
|
||||
return defer.fail(Exception("Key or fingerprint mismatch"))
|
||||
|
||||
def connectionSecure(self):
|
||||
self.requestService(ConchTestClientAuth(b'testuser',
|
||||
ConchTestClientConnection(self._channelFactory)))
|
||||
|
||||
|
||||
class ConchTestClientAuth(userauth.SSHUserAuthClient):
|
||||
|
||||
hasTriedNone = 0 # have we tried the 'none' auth yet?
|
||||
canSucceedPublicKey = 0 # can we succeed with this yet?
|
||||
canSucceedPassword = 0
|
||||
|
||||
def ssh_USERAUTH_SUCCESS(self, packet):
|
||||
if not self.canSucceedPassword and self.canSucceedPublicKey:
|
||||
raise unittest.FailTest(
|
||||
'got USERAUTH_SUCCESS before password and publickey')
|
||||
userauth.SSHUserAuthClient.ssh_USERAUTH_SUCCESS(self, packet)
|
||||
|
||||
def getPassword(self):
|
||||
self.canSucceedPassword = 1
|
||||
return defer.succeed(b'testpass')
|
||||
|
||||
def getPrivateKey(self):
|
||||
self.canSucceedPublicKey = 1
|
||||
return defer.succeed(keys.Key.fromString(privateDSA_openssh))
|
||||
|
||||
def getPublicKey(self):
|
||||
return keys.Key.fromString(publicDSA_openssh)
|
||||
|
||||
|
||||
class ConchTestClientConnection(connection.SSHConnection):
|
||||
"""
|
||||
@ivar _completed: A L{Deferred} which will be fired when the number of
|
||||
results collected reaches C{totalResults}.
|
||||
"""
|
||||
name = b'ssh-connection'
|
||||
results = 0
|
||||
totalResults = 8
|
||||
|
||||
def __init__(self, channelFactory):
|
||||
connection.SSHConnection.__init__(self)
|
||||
self._channelFactory = channelFactory
|
||||
|
||||
def serviceStarted(self):
|
||||
self.openChannel(self._channelFactory(conn=self))
|
||||
|
||||
|
||||
class SSHTestChannel(channel.SSHChannel):
|
||||
|
||||
def __init__(self, name, opened, *args, **kwargs):
|
||||
self.name = name
|
||||
self._opened = opened
|
||||
self.received = []
|
||||
self.receivedExt = []
|
||||
self.onClose = defer.Deferred()
|
||||
channel.SSHChannel.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
def openFailed(self, reason):
|
||||
self._opened.errback(reason)
|
||||
|
||||
|
||||
def channelOpen(self, ignore):
|
||||
self._opened.callback(self)
|
||||
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.received.append(data)
|
||||
|
||||
|
||||
def extReceived(self, dataType, data):
|
||||
if dataType == connection.EXTENDED_DATA_STDERR:
|
||||
self.receivedExt.append(data)
|
||||
else:
|
||||
log.msg("Unrecognized extended data: %r" % (dataType,))
|
||||
|
||||
|
||||
def request_exit_status(self, status):
|
||||
[self.status] = struct.unpack('>L', status)
|
||||
|
||||
|
||||
def eofReceived(self):
|
||||
self.eofCalled = True
|
||||
|
||||
|
||||
def closed(self):
|
||||
self.onClose.callback(None)
|
||||
|
||||
|
||||
def conchTestPublicKeyChecker():
|
||||
"""
|
||||
Produces a SSHPublicKeyChecker with an in-memory key mapping with
|
||||
a single use: 'testuser'
|
||||
|
||||
@return: L{twisted.conch.checkers.SSHPublicKeyChecker}
|
||||
"""
|
||||
conchTestPublicKeyDB = checkers.InMemorySSHKeyDB(
|
||||
{b'testuser': [keys.Key.fromString(publicDSA_openssh)]})
|
||||
return checkers.SSHPublicKeyChecker(conchTestPublicKeyDB)
|
||||
|
||||
|
||||
|
||||
class SSHProtocolTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for communication between L{SSHServerTransport} and
|
||||
L{SSHClientTransport}.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not pyasn1:
|
||||
skip = "Cannot run without PyASN1"
|
||||
|
||||
def _ourServerOurClientTest(self, name=b'session', **kwargs):
|
||||
"""
|
||||
Create a connected SSH client and server protocol pair and return a
|
||||
L{Deferred} which fires with an L{SSHTestChannel} instance connected to
|
||||
a channel on that SSH connection.
|
||||
"""
|
||||
result = defer.Deferred()
|
||||
self.realm = ConchTestRealm(b'testuser')
|
||||
p = portal.Portal(self.realm)
|
||||
sshpc = ConchTestSSHChecker()
|
||||
sshpc.registerChecker(ConchTestPasswordChecker())
|
||||
sshpc.registerChecker(conchTestPublicKeyChecker())
|
||||
p.registerChecker(sshpc)
|
||||
fac = ConchTestServerFactory()
|
||||
fac.portal = p
|
||||
fac.startFactory()
|
||||
self.server = fac.buildProtocol(None)
|
||||
self.clientTransport = LoopbackRelay(self.server)
|
||||
self.client = ConchTestClient(
|
||||
lambda conn: SSHTestChannel(name, result, conn=conn, **kwargs))
|
||||
|
||||
self.serverTransport = LoopbackRelay(self.client)
|
||||
|
||||
self.server.makeConnection(self.serverTransport)
|
||||
self.client.makeConnection(self.clientTransport)
|
||||
return result
|
||||
|
||||
|
||||
def test_subsystemsAndGlobalRequests(self):
|
||||
"""
|
||||
Run the Conch server against the Conch client. Set up several different
|
||||
channels which exercise different behaviors and wait for them to
|
||||
complete. Verify that the channels with errors log them.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest()
|
||||
|
||||
def cbSubsystem(channel):
|
||||
self.channel = channel
|
||||
return self.assertFailure(
|
||||
channel.conn.sendRequest(
|
||||
channel, b'subsystem', common.NS(b'not-crazy'), 1),
|
||||
Exception)
|
||||
channel.addCallback(cbSubsystem)
|
||||
|
||||
def cbNotCrazyFailed(ignored):
|
||||
channel = self.channel
|
||||
return channel.conn.sendRequest(
|
||||
channel, b'subsystem', common.NS(b'crazy'), 1)
|
||||
channel.addCallback(cbNotCrazyFailed)
|
||||
|
||||
def cbGlobalRequests(ignored):
|
||||
channel = self.channel
|
||||
d1 = channel.conn.sendGlobalRequest(b'foo', b'bar', 1)
|
||||
|
||||
d2 = channel.conn.sendGlobalRequest(b'foo-2', b'bar2', 1)
|
||||
d2.addCallback(self.assertEqual, b'data')
|
||||
|
||||
d3 = self.assertFailure(
|
||||
channel.conn.sendGlobalRequest(b'bar', b'foo', 1),
|
||||
Exception)
|
||||
|
||||
return defer.gatherResults([d1, d2, d3])
|
||||
channel.addCallback(cbGlobalRequests)
|
||||
|
||||
def disconnect(ignored):
|
||||
self.assertEqual(
|
||||
self.realm.avatar.globalRequests,
|
||||
{"foo": b"bar", "foo_2": b"bar2"})
|
||||
channel = self.channel
|
||||
channel.conn.transport.expectedLoseConnection = True
|
||||
channel.conn.serviceStopped()
|
||||
channel.loseConnection()
|
||||
channel.addCallback(disconnect)
|
||||
|
||||
return channel
|
||||
|
||||
|
||||
def test_shell(self):
|
||||
"""
|
||||
L{SSHChannel.sendRequest} can open a shell with a I{pty-req} request,
|
||||
specifying a terminal type and window size.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest()
|
||||
|
||||
data = session.packRequest_pty_req(
|
||||
b'conch-test-term', (24, 80, 0, 0), b'')
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return channel.conn.sendRequest(channel, b'pty-req', data, 1)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbPty(ignored):
|
||||
# The server-side object corresponding to our client side channel.
|
||||
session = self.realm.avatar.conn.channels[0].session
|
||||
self.assertIs(session.avatar, self.realm.avatar)
|
||||
self.assertEqual(session._terminalType, b'conch-test-term')
|
||||
self.assertEqual(session._windowSize, (24, 80, 0, 0))
|
||||
self.assertTrue(session.ptyReq)
|
||||
channel = self.channel
|
||||
return channel.conn.sendRequest(channel, b'shell', b'', 1)
|
||||
channel.addCallback(cbPty)
|
||||
|
||||
def cbShell(ignored):
|
||||
self.channel.write(b'testing the shell!\x00')
|
||||
self.channel.conn.sendEOF(self.channel)
|
||||
return defer.gatherResults([
|
||||
self.channel.onClose,
|
||||
self.realm.avatar._testSession.onClose])
|
||||
channel.addCallback(cbShell)
|
||||
|
||||
def cbExited(ignored):
|
||||
if self.channel.status != 0:
|
||||
log.msg(
|
||||
'shell exit status was not 0: %i' % (self.channel.status,))
|
||||
self.assertEqual(
|
||||
b"".join(self.channel.received),
|
||||
b'testing the shell!\x00\r\n')
|
||||
self.assertTrue(self.channel.eofCalled)
|
||||
self.assertTrue(
|
||||
self.realm.avatar._testSession.eof)
|
||||
channel.addCallback(cbExited)
|
||||
return channel
|
||||
|
||||
|
||||
def test_failedExec(self):
|
||||
"""
|
||||
If L{SSHChannel.sendRequest} issues an exec which the server responds to
|
||||
with an error, the L{Deferred} it returns fires its errback.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest()
|
||||
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return self.assertFailure(
|
||||
channel.conn.sendRequest(
|
||||
channel, b'exec', common.NS(b'jumboliah'), 1),
|
||||
Exception)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbFailed(ignored):
|
||||
# The server logs this exception when it cannot perform the
|
||||
# requested exec.
|
||||
errors = self.flushLoggedErrors(error.ConchError)
|
||||
self.assertEqual(errors[0].value.args, ('bad exec', None))
|
||||
channel.addCallback(cbFailed)
|
||||
return channel
|
||||
|
||||
|
||||
def test_falseChannel(self):
|
||||
"""
|
||||
When the process started by a L{SSHChannel.sendRequest} exec request
|
||||
exits, the exit status is reported to the channel.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest()
|
||||
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return channel.conn.sendRequest(
|
||||
channel, b'exec', common.NS(b'false'), 1)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbExec(ignored):
|
||||
return self.channel.onClose
|
||||
channel.addCallback(cbExec)
|
||||
|
||||
def cbClosed(ignored):
|
||||
# No data is expected
|
||||
self.assertEqual(self.channel.received, [])
|
||||
self.assertNotEqual(self.channel.status, 0)
|
||||
channel.addCallback(cbClosed)
|
||||
return channel
|
||||
|
||||
|
||||
def test_errorChannel(self):
|
||||
"""
|
||||
Bytes sent over the extended channel for stderr data are delivered to
|
||||
the channel's C{extReceived} method.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest(localWindow=4, localMaxPacket=5)
|
||||
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return channel.conn.sendRequest(
|
||||
channel, b'exec', common.NS(b'eecho hello'), 1)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbExec(ignored):
|
||||
return defer.gatherResults([
|
||||
self.channel.onClose,
|
||||
self.realm.avatar._testSession.onClose])
|
||||
channel.addCallback(cbExec)
|
||||
|
||||
def cbClosed(ignored):
|
||||
self.assertEqual(self.channel.received, [])
|
||||
self.assertEqual(b"".join(self.channel.receivedExt), b"hello\r\n")
|
||||
self.assertEqual(self.channel.status, 0)
|
||||
self.assertTrue(self.channel.eofCalled)
|
||||
self.assertEqual(self.channel.localWindowLeft, 4)
|
||||
self.assertEqual(
|
||||
self.channel.localWindowLeft,
|
||||
self.realm.avatar._testSession.remoteWindowLeftAtClose)
|
||||
channel.addCallback(cbClosed)
|
||||
return channel
|
||||
|
||||
|
||||
def test_unknownChannel(self):
|
||||
"""
|
||||
When an attempt is made to open an unknown channel type, the L{Deferred}
|
||||
returned by L{SSHChannel.sendRequest} fires its errback.
|
||||
"""
|
||||
d = self.assertFailure(
|
||||
self._ourServerOurClientTest(b'crazy-unknown-channel'), Exception)
|
||||
def cbFailed(ignored):
|
||||
errors = self.flushLoggedErrors(error.ConchError)
|
||||
self.assertEqual(errors[0].value.args, (3, 'unknown channel'))
|
||||
self.assertEqual(len(errors), 1)
|
||||
d.addCallback(cbFailed)
|
||||
return d
|
||||
|
||||
|
||||
def test_maxPacket(self):
|
||||
"""
|
||||
An L{SSHChannel} can be configured with a maximum packet size to
|
||||
receive.
|
||||
"""
|
||||
# localWindow needs to be at least 11 otherwise the assertion about it
|
||||
# in cbClosed is invalid.
|
||||
channel = self._ourServerOurClientTest(
|
||||
localWindow=11, localMaxPacket=1)
|
||||
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return channel.conn.sendRequest(
|
||||
channel, b'exec', common.NS(b'secho hello'), 1)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbExec(ignored):
|
||||
return self.channel.onClose
|
||||
channel.addCallback(cbExec)
|
||||
|
||||
def cbClosed(ignored):
|
||||
self.assertEqual(self.channel.status, 0)
|
||||
self.assertEqual(b"".join(self.channel.received), b"hello\r\n")
|
||||
self.assertEqual(b"".join(self.channel.receivedExt), b"hello\r\n")
|
||||
self.assertEqual(self.channel.localWindowLeft, 11)
|
||||
self.assertTrue(self.channel.eofCalled)
|
||||
channel.addCallback(cbClosed)
|
||||
return channel
|
||||
|
||||
|
||||
def test_echo(self):
|
||||
"""
|
||||
Normal standard out bytes are sent to the channel's C{dataReceived}
|
||||
method.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest(localWindow=4, localMaxPacket=5)
|
||||
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return channel.conn.sendRequest(
|
||||
channel, b'exec', common.NS(b'echo hello'), 1)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbEcho(ignored):
|
||||
return defer.gatherResults([
|
||||
self.channel.onClose,
|
||||
self.realm.avatar._testSession.onClose])
|
||||
channel.addCallback(cbEcho)
|
||||
|
||||
def cbClosed(ignored):
|
||||
self.assertEqual(self.channel.status, 0)
|
||||
self.assertEqual(b"".join(self.channel.received), b"hello\r\n")
|
||||
self.assertEqual(self.channel.localWindowLeft, 4)
|
||||
self.assertTrue(self.channel.eofCalled)
|
||||
self.assertEqual(
|
||||
self.channel.localWindowLeft,
|
||||
self.realm.avatar._testSession.remoteWindowLeftAtClose)
|
||||
channel.addCallback(cbClosed)
|
||||
return channel
|
||||
|
||||
|
||||
|
||||
class SSHFactoryTests(unittest.TestCase):
|
||||
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not pyasn1:
|
||||
skip = "Cannot run without PyASN1"
|
||||
|
||||
def makeSSHFactory(self, primes=None):
|
||||
sshFactory = factory.SSHFactory()
|
||||
gpk = lambda: {'ssh-rsa' : keys.Key(None)}
|
||||
sshFactory.getPrimes = lambda: primes
|
||||
sshFactory.getPublicKeys = sshFactory.getPrivateKeys = gpk
|
||||
sshFactory.startFactory()
|
||||
return sshFactory
|
||||
|
||||
|
||||
def test_buildProtocol(self):
|
||||
"""
|
||||
By default, buildProtocol() constructs an instance of
|
||||
SSHServerTransport.
|
||||
"""
|
||||
factory = self.makeSSHFactory()
|
||||
protocol = factory.buildProtocol(None)
|
||||
self.assertIsInstance(protocol, transport.SSHServerTransport)
|
||||
|
||||
|
||||
def test_buildProtocolRespectsProtocol(self):
|
||||
"""
|
||||
buildProtocol() calls 'self.protocol()' to construct a protocol
|
||||
instance.
|
||||
"""
|
||||
calls = []
|
||||
def makeProtocol(*args):
|
||||
calls.append(args)
|
||||
return transport.SSHServerTransport()
|
||||
factory = self.makeSSHFactory()
|
||||
factory.protocol = makeProtocol
|
||||
factory.buildProtocol(None)
|
||||
self.assertEqual([()], calls)
|
||||
|
||||
|
||||
def test_buildProtocolNoPrimes(self):
|
||||
"""
|
||||
Group key exchanges are not supported when we don't have the primes
|
||||
database.
|
||||
"""
|
||||
f1 = self.makeSSHFactory(primes=None)
|
||||
|
||||
p1 = f1.buildProtocol(None)
|
||||
|
||||
self.assertNotIn(
|
||||
b'diffie-hellman-group-exchange-sha1', p1.supportedKeyExchanges)
|
||||
self.assertNotIn(
|
||||
b'diffie-hellman-group-exchange-sha256', p1.supportedKeyExchanges)
|
||||
|
||||
|
||||
def test_buildProtocolWithPrimes(self):
|
||||
"""
|
||||
Group key exchanges are supported when we have the primes database.
|
||||
"""
|
||||
f2 = self.makeSSHFactory(primes={1:(2,3)})
|
||||
|
||||
p2 = f2.buildProtocol(None)
|
||||
|
||||
self.assertIn(
|
||||
b'diffie-hellman-group-exchange-sha1', p2.supportedKeyExchanges)
|
||||
self.assertIn(
|
||||
b'diffie-hellman-group-exchange-sha256', p2.supportedKeyExchanges)
|
||||
|
||||
|
||||
|
||||
class MPTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{common.getMP}.
|
||||
|
||||
@cvar getMP: a method providing a MP parser.
|
||||
@type getMP: C{callable}
|
||||
"""
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not pyasn1:
|
||||
skip = "Cannot run without PyASN1"
|
||||
|
||||
if cryptography:
|
||||
getMP = staticmethod(common.getMP)
|
||||
|
||||
def test_getMP(self):
|
||||
"""
|
||||
L{common.getMP} should parse the a multiple precision integer from a
|
||||
string: a 4-byte length followed by length bytes of the integer.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.getMP(b'\x00\x00\x00\x04\x00\x00\x00\x01'),
|
||||
(1, b''))
|
||||
|
||||
|
||||
def test_getMPBigInteger(self):
|
||||
"""
|
||||
L{common.getMP} should be able to parse a big enough integer
|
||||
(that doesn't fit on one byte).
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.getMP(b'\x00\x00\x00\x04\x01\x02\x03\x04'),
|
||||
(16909060, b''))
|
||||
|
||||
|
||||
def test_multipleGetMP(self):
|
||||
"""
|
||||
L{common.getMP} has the ability to parse multiple integer in the same
|
||||
string.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.getMP(b'\x00\x00\x00\x04\x00\x00\x00\x01'
|
||||
b'\x00\x00\x00\x04\x00\x00\x00\x02', 2),
|
||||
(1, 2, b''))
|
||||
|
||||
|
||||
def test_getMPRemainingData(self):
|
||||
"""
|
||||
When more data than needed is sent to L{common.getMP}, it should return
|
||||
the remaining data.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.getMP(b'\x00\x00\x00\x04\x00\x00\x00\x01foo'),
|
||||
(1, b'foo'))
|
||||
|
||||
|
||||
def test_notEnoughData(self):
|
||||
"""
|
||||
When the string passed to L{common.getMP} doesn't even make 5 bytes,
|
||||
it should raise a L{struct.error}.
|
||||
"""
|
||||
self.assertRaises(struct.error, self.getMP, b'\x02\x00')
|
||||
|
||||
|
||||
class GMPYInstallDeprecationTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for the deprecation of former GMPY accidental public API.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "cannot run without cryptography"
|
||||
|
||||
def test_deprecated(self):
|
||||
"""
|
||||
L{twisted.conch.ssh.common.install} is deprecated.
|
||||
"""
|
||||
common.install()
|
||||
warnings = self.flushWarnings([self.test_deprecated])
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(
|
||||
warnings[0]["message"],
|
||||
"twisted.conch.ssh.common.install was deprecated in Twisted 16.5.0"
|
||||
)
|
||||
Reference in New Issue
Block a user