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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const {
} = require('./protocol/constants.js');
const { init: cryptoInit } = require('./protocol/crypto.js');
const Protocol = require('./protocol/Protocol.js');
const { parseKey } = require('./protocol/keyParser.js');
const { isParsedKey, parseKey } = require('./protocol/keyParser.js');
const { SFTP } = require('./protocol/SFTP.js');
const {
bufferCopy,
Expand Down Expand Up @@ -207,8 +207,16 @@ class Client extends EventEmitter {
: undefined);
this.config.privateKey = (typeof cfg.privateKey === 'string'
|| Buffer.isBuffer(cfg.privateKey)
|| isParsedKey(cfg.privateKey)
? cfg.privateKey
: undefined);
// publicKey may be a certificate file (e.g. id_ed25519-cert.pub) to use
// for identification while privateKey is used for signing.
this.config.publicKey = (typeof cfg.publicKey === 'string'
|| Buffer.isBuffer(cfg.publicKey)
|| isParsedKey(cfg.publicKey)
? cfg.publicKey
: undefined);
this.config.localHostname = (typeof cfg.localHostname === 'string'
? cfg.localHostname
: undefined);
Expand Down Expand Up @@ -254,6 +262,7 @@ class Client extends EventEmitter {
this._agent = (this.config.agent ? this.config.agent : undefined);
this._remoteVer = undefined;
let privateKey;
let authKey; // Key object used for publickey auth (may be a certificate)

if (this.config.privateKey) {
privateKey = parseKey(this.config.privateKey, cfg.passphrase);
Expand All @@ -268,6 +277,18 @@ class Client extends EventEmitter {
'privateKey value does not contain a (valid) private key'
);
}

if (this.config.publicKey) {
// A separate certificate was provided for identification. Use it as
// the key sent to the server while privateKey is used for signing.
authKey = parseKey(this.config.publicKey);
if (authKey instanceof Error)
throw new Error(`Cannot parse publicKey: ${authKey.message}`);
if (Array.isArray(authKey))
authKey = authKey[0];
} else {
authKey = privateKey;
}
}

let hostVerifier;
Expand Down Expand Up @@ -462,7 +483,10 @@ class Client extends EventEmitter {
});
} else if (curAuth.type === 'publickey') {
proto.authPK(curAuth.username, curAuth.key, keyAlgo, (buf, cb) => {
const signature = curAuth.key.sign(buf, hashAlgo);
// Sign with privateKey (may differ from curAuth.key when a
// certificate is used for identification).
const signingKey = curAuth.privateKey || curAuth.key;
const signature = signingKey.sign(buf, hashAlgo);
if (signature instanceof Error) {
signature.message =
`Error signing data with key: ${signature.message}`;
Expand Down Expand Up @@ -882,13 +906,14 @@ class Client extends EventEmitter {
nextAuth = { type, username, password: this.config.password };
break;
case 'publickey':
nextAuth = { type, username, key: privateKey };
nextAuth = { type, username, key: authKey, privateKey };
break;
case 'hostbased':
nextAuth = {
type,
username,
key: privateKey,
key: authKey,
privateKey,
localHostname: this.config.localHostname,
localUsername: this.config.localUsername,
};
Expand Down
21 changes: 15 additions & 6 deletions lib/protocol/Protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,15 @@ class Protocol {

const userLen = Buffer.byteLength(username);
const algoLen = Buffer.byteLength(keyAlgo);
// OpenSSH certificates: the request's algorithm field names the certificate
// ("ssh-ed25519-cert-v01@openssh.com"), but the signature blob must name
// the plain algorithm the key actually signs with ("ssh-ed25519"). For
// plain keys the two are identical and nothing below changes.
const certSuffix = '-cert-v01@openssh.com';
const sigAlgo = keyAlgo.endsWith(certSuffix)
? keyAlgo.slice(0, -certSuffix.length)
: keyAlgo;
const sigAlgoLen = Buffer.byteLength(sigAlgo);
const pubKeyLen = pubKey.length;
const sessionID = this._kex.sessionID;
const sesLen = sessionID.length;
Expand Down Expand Up @@ -697,15 +706,15 @@ class Protocol {
}

cbSign(packet, (signature) => {
signature = convertSignature(signature, keyType);
signature = convertSignature(signature, sigAlgo);
if (signature === false)
throw new Error('Error while converting handshake signature');

const sigLen = signature.length;
p = this._packetRW.write.allocStart;
packet = this._packetRW.write.alloc(
1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen + 4
+ 4 + algoLen + 4 + sigLen
+ 4 + sigAlgoLen + 4 + sigLen
);

// TODO: simply copy from original "packet" to new `packet` to avoid
Expand All @@ -729,12 +738,12 @@ class Protocol {
writeUInt32BE(packet, pubKeyLen, p += algoLen);
packet.set(pubKey, p += 4);

writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += pubKeyLen);
writeUInt32BE(packet, 4 + sigAlgoLen + 4 + sigLen, p += pubKeyLen);

writeUInt32BE(packet, algoLen, p += 4);
packet.utf8Write(keyAlgo, p += 4, algoLen);
writeUInt32BE(packet, sigAlgoLen, p += 4);
packet.utf8Write(sigAlgo, p += 4, sigAlgoLen);

writeUInt32BE(packet, sigLen, p += algoLen);
writeUInt32BE(packet, sigLen, p += sigAlgoLen);
packet.set(signature, p += 4);

// Servers shouldn't send packet type 60 in response to signed publickey
Expand Down
46 changes: 38 additions & 8 deletions lib/protocol/keyParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,21 @@ function OpenSSH_Public(type, comment, pubPEM, pubSSH, algo) {
this[SYM_DECRYPTED] = false;
}
OpenSSH_Public.prototype = BaseKey;

// Represents an SSH certificate (e.g. ssh-ed25519-cert-v01@openssh.com).
// The full certificate blob is stored as-is and returned from getPublicSSH()
// so it can be used directly in publickey auth requests.
function Cert_Public(type, comment, certBlob) {
this.type = type;
this.comment = comment;
this[SYM_PRIV_PEM] = null;
this[SYM_PUB_PEM] = null;
this[SYM_PUB_SSH] = certBlob;
this[SYM_HASH_ALGO] = null;
this[SYM_DECRYPTED] = false;
}
Cert_Public.prototype = BaseKey;

{
let regexp;
if (eddsaSupported)
Expand All @@ -1198,7 +1213,7 @@ OpenSSH_Public.prototype = BaseKey;
return null;
// m[1] = full type
// m[2] = base type
// m[3] = base64-encoded public key
// m[3] = base64-encoded public key / certificate blob
// m[4] = comment

const fullType = m[1];
Expand All @@ -1210,6 +1225,15 @@ OpenSSH_Public.prototype = BaseKey;
if (type === undefined || type.indexOf(baseType) !== 0)
return new Error('Malformed OpenSSH public key');

// Certificate: preserve the full blob so getPublicSSH() returns it intact
// for use in SSH_MSG_USERAUTH_REQUEST publickey auth. The type sniff above
// left the parser's `_pos` cursor on `data`; it must not escape with the
// blob, where it would make two identical certificates compare unequal.
if (fullType !== baseType) {
delete data._pos;
return new Cert_Public(fullType, comment, data);
}

return parseDER(data, baseType, comment, fullType);
};
}
Expand Down Expand Up @@ -1458,13 +1482,18 @@ function parseKey(data, passphrase) {
binaryKeyParser.init(origBuffer, 0);
const type = binaryKeyParser.readString(true);
if (type !== undefined) {
data = binaryKeyParser.readRaw();
if (data !== undefined) {
ret = parseDER(data, type, '', type);
// Ignore potentially useless errors in case the data was not actually
// in the binary format
if (ret instanceof Error)
ret = null;
// Certificate blob: preserve it whole rather than parsing its internals
if (type.indexOf('-cert-v0') !== -1) {
ret = new Cert_Public(type, '', origBuffer);
} else {
data = binaryKeyParser.readRaw();
if (data !== undefined) {
ret = parseDER(data, type, '', type);
// Ignore potentially useless errors in case the data was not actually
// in the binary format
if (ret instanceof Error)
ret = null;
}
}
}
binaryKeyParser.clear();
Expand All @@ -1478,6 +1507,7 @@ function parseKey(data, passphrase) {

module.exports = {
isParsedKey,
Cert_Public,
isSupportedKeyType,
parseDERKey: (data, type) => parseDER(data, type, '', type),
parseKey,
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/id_ecdsa_cert_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRmVDua5cBz9KzJqMt7Iur/wiJcvxGE
OPSz26h0HA/QrZL58SnuNIPwNuNkRe3gD6OvVNIgY+0ugEoOtA2td1wZAAAAsEcVnTVHFZ
01AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGZUO5rlwHP0rMmo
y3si6v/CIly/EYQ49LPbqHQcD9CtkvnxKe40g/A242RF7eAPo69U0iBj7S6ASg60Da13XB
kAAAAgVNtyZE0ka1Uca8Doi9MK+zmnWLgrfVFWi0YMuHW8pSUAAAAWc3NoMiBjZXJ0IHRl
c3QgKGVjZHNhKQEC
-----END OPENSSH PRIVATE KEY-----
1 change: 1 addition & 0 deletions test/fixtures/id_ecdsa_cert_key-cert.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgTak0HL5RJH9zw9YlAF1gSZcxg1n4REuqJdW6pkkmVg0AAAAIbmlzdHAyNTYAAABBBGZUO5rlwHP0rMmoy3si6v/CIly/EYQ49LPbqHQcD9CtkvnxKe40g/A242RF7eAPo69U0iBj7S6ASg60Da13XBkAAAAAAAAAAAAAAAEAAAAPc3NoMi10ZXN0LWVjZHNhAAAADQAAAAlDZXJ0IFVzZXIAAAAAXgu20AAAAAGwne7QAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAg8pFFhI+jq1LhsqtKalxN4zigUFvWZFVIO5rW6WF+q80AAABTAAAAC3NzaC1lZDI1NTE5AAAAQPays/Qg1YOopt5YdWxA6vbkdxsjqzcGI+I0o421mvteSo2KiBmBCqFFMGaIoJz84AbsqSXEA4ceIw9RqB4+TwQ= ssh2 cert test (ecdsa)
1 change: 1 addition & 0 deletions test/fixtures/id_ecdsa_cert_key.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGZUO5rlwHP0rMmoy3si6v/CIly/EYQ49LPbqHQcD9CtkvnxKe40g/A242RF7eAPo69U0iBj7S6ASg60Da13XBk= ssh2 cert test (ecdsa)
7 changes: 7 additions & 0 deletions test/fixtures/id_ed25519_cert_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDi3+ww7F3/p3bj95kkS5a3l0bLSvqLwBusCn4ZCs1jxgAAAKCREaswkRGr
MAAAAAtzc2gtZWQyNTUxOQAAACDi3+ww7F3/p3bj95kkS5a3l0bLSvqLwBusCn4ZCs1jxg
AAAEAbGT1n6QEvc47JS9PWzdz11zGRKyiB8cDVivKsr+bz7uLf7DDsXf+nduP3mSRLlreX
RstK+ovAG6wKfhkKzWPGAAAAGHNzaDIgY2VydCB0ZXN0IChlZDI1NTE5KQECAwQF
-----END OPENSSH PRIVATE KEY-----
1 change: 1 addition & 0 deletions test/fixtures/id_ed25519_cert_key-cert.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAILi3YOlmrVL3wgZpeVUeLdd7jxRkM7vGbxgyl0xtiiiKAAAAIOLf7DDsXf+nduP3mSRLlreXRstK+ovAG6wKfhkKzWPGAAAAAAAAAAAAAAABAAAAEXNzaDItdGVzdC1lZDI1NTE5AAAADQAAAAlDZXJ0IFVzZXIAAAAAXgu20AAAAAGwne7QAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAg8pFFhI+jq1LhsqtKalxN4zigUFvWZFVIO5rW6WF+q80AAABTAAAAC3NzaC1lZDI1NTE5AAAAQCss4EFFBJrt9gkvhsRKlt3B96jkNymbyM5QBmVPK7CAukvFx+R+Bx+HIg2Cu1/3kJy9lhixISAkIzn+3MEStwE= ssh2 cert test (ed25519)
1 change: 1 addition & 0 deletions test/fixtures/id_ed25519_cert_key.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOLf7DDsXf+nduP3mSRLlreXRstK+ovAG6wKfhkKzWPG ssh2 cert test (ed25519)
27 changes: 27 additions & 0 deletions test/fixtures/id_rsa_cert_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEA2ee/j51zvZRGtIqTz4giqpdfc0mhOIuE3mvEYR4XOCxCBPUdN3Pn
WNpkhlw26uLdqkOP96eRHn5Vv4roj33soHIT5fJ2sJ0s7NmiIpkRBhDe1cmafm4vFZidex
/9Xsyba4JLp+IeyC2GA/3qLn5DoFnJInDgy+jvY6nbCGyaF9ZFSaaNaF9+TLN1qyP51svf
rTh5umYM9AVT6JqERsOQGadW/fHwaBauUJ2Lnos8r9/v81ZDOH8KJW4dLqWUTvE22AcAGf
A6LuMUBmM01FzovYQ01N158nizCzS1JyBH24vN98pZe/ft0npbmXw86g0OCy9ZUTmybYd7
uRxmJrXrGQAAA9D/VUcP/1VHDwAAAAdzc2gtcnNhAAABAQDZ57+PnXO9lEa0ipPPiCKql1
9zSaE4i4Tea8RhHhc4LEIE9R03c+dY2mSGXDbq4t2qQ4/3p5EeflW/iuiPfeygchPl8naw
nSzs2aIimREGEN7VyZp+bi8VmJ17H/1ezJtrgkun4h7ILYYD/eoufkOgWckicODL6O9jqd
sIbJoX1kVJpo1oX35Ms3WrI/nWy9+tOHm6Zgz0BVPomoRGw5AZp1b98fBoFq5QnYueizyv
3+/zVkM4fwolbh0upZRO8TbYBwAZ8Dou4xQGYzTUXOi9hDTU3XnyeLMLNLUnIEfbi833yl
l79+3SeluZfDzqDQ4LL1lRObJth3u5HGYmtesZAAAAAwEAAQAAAQEAucGxr4AN9oK8c5Pe
xX/L7Zj1KZaO9WEad3FvC0tXh+9SqF879NW9ViV2pINf3YRgapEF3ZzfPRt9hbeo4Qn7+h
rFk7TKMW0Lqy0r5kqOFJm1HJKsPTM4uDRNq3Rtza6mn1OHoypEC2mjYSvHwrKoe72OlOmc
+Lctu8xEiD3sbnwK1cTKw/nFuIuGRmhxh4W1wrjA4cHNH8AmWPuPjCZXNbzi3BEt2RRX1k
hMsWq+JfPlEGiPJKkqVRLDJkG/XABcpnCjgeMieqJJwcjNSL0PBvStjdJ8OlYPUJPA+BAa
d/ZB1nKJ1ARf7dh5VqS20nrvxOitcECDDJFkgfpwE8lwwQAAAIEA2V59XkQO/iuuLexvyU
PABCFIShx0QM0PF1GRABYeikXxrtaPSiYkf7iEDsAd45Ah/X6iFYOwxBHLGW3PNeLWuKmW
le58f1reH468jB659oPMIwpanzo9pw/gywTtx0QJUQAc4BWGYj/8XQSax2+yuCWGSeGTMu
DnRRljTd5nr+AAAACBAP9FfcJ/iNvDoNTUqOZr1LM/3DwsZsK2NFB046uXEmUtkVMOXW+F
eWbIiFf+lV1OvFdYgD1TXoGOiUyREtwQv0W7KNH9qR2MA43S1YLBVxvfaBsrQd2dOCInWD
dGVJY1cKRT7aPSOurXI8GksT67KJbPetVKJYy2gupveWkgSwVtAAAAgQDahvTKgLWZMgXl
JA37f65BVqhYC/pYqMKGJnxbPpgPWuunjcIrm9Yc+mZP0z9RjA5WzXitB+VH4Mp+WcU1Xd
BRrMyJ7UCDA3HXmYrNzITWzURPwg72p3ca8yIwgzi9SZQR/hf77KWpLaGIo7JHX83kfhvX
6jOjvVbcBHyLEGGs3QAAABRzc2gyIGNlcnQgdGVzdCAocnNhKQECAwQF
-----END OPENSSH PRIVATE KEY-----
1 change: 1 addition & 0 deletions test/fixtures/id_rsa_cert_key-cert.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgoRYzrqRIH7VnxQSKjgCnQ0cBR7NlIPO88eOqY8x/8bMAAAADAQABAAABAQDZ57+PnXO9lEa0ipPPiCKql19zSaE4i4Tea8RhHhc4LEIE9R03c+dY2mSGXDbq4t2qQ4/3p5EeflW/iuiPfeygchPl8nawnSzs2aIimREGEN7VyZp+bi8VmJ17H/1ezJtrgkun4h7ILYYD/eoufkOgWckicODL6O9jqdsIbJoX1kVJpo1oX35Ms3WrI/nWy9+tOHm6Zgz0BVPomoRGw5AZp1b98fBoFq5QnYueizyv3+/zVkM4fwolbh0upZRO8TbYBwAZ8Dou4xQGYzTUXOi9hDTU3XnyeLMLNLUnIEfbi833yll79+3SeluZfDzqDQ4LL1lRObJth3u5HGYmtesZAAAAAAAAAAAAAAABAAAADXNzaDItdGVzdC1yc2EAAAANAAAACUNlcnQgVXNlcgAAAABeC7bQAAAAAbCd7tAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDykUWEj6OrUuGyq0pqXE3jOKBQW9ZkVUg7mtbpYX6rzQAAAFMAAAALc3NoLWVkMjU1MTkAAABAbL5jRF146WtpsOR7wN6NM3N7IqUqOLRI8MfTFeeVm2YwGFa4nnCUqO8Qi84geZYD6vhIS43Wct+ltAr5tYsqDQ== ssh2 cert test (rsa)
1 change: 1 addition & 0 deletions test/fixtures/id_rsa_cert_key.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZ57+PnXO9lEa0ipPPiCKql19zSaE4i4Tea8RhHhc4LEIE9R03c+dY2mSGXDbq4t2qQ4/3p5EeflW/iuiPfeygchPl8nawnSzs2aIimREGEN7VyZp+bi8VmJ17H/1ezJtrgkun4h7ILYYD/eoufkOgWckicODL6O9jqdsIbJoX1kVJpo1oX35Ms3WrI/nWy9+tOHm6Zgz0BVPomoRGw5AZp1b98fBoFq5QnYueizyv3+/zVkM4fwolbh0upZRO8TbYBwAZ8Dou4xQGYzTUXOi9hDTU3XnyeLMLNLUnIEfbi833yll79+3SeluZfDzqDQ4LL1lRObJth3u5HGYmtesZ ssh2 cert test (rsa)
7 changes: 7 additions & 0 deletions test/fixtures/ssh_user_ca
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDykUWEj6OrUuGyq0pqXE3jOKBQW9ZkVUg7mtbpYX6rzQAAAJCLILSGiyC0
hgAAAAtzc2gtZWQyNTUxOQAAACDykUWEj6OrUuGyq0pqXE3jOKBQW9ZkVUg7mtbpYX6rzQ
AAAECxVlsIUGiJCdmvEtE2z2+cPi/ChWjf8gSfOHcCm8JYU/KRRYSPo6tS4bKrSmpcTeM4
oFBb1mRVSDua1ulhfqvNAAAADHNzaDIgdGVzdCBDQQE=
-----END OPENSSH PRIVATE KEY-----
1 change: 1 addition & 0 deletions test/fixtures/ssh_user_ca.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPKRRYSPo6tS4bKrSmpcTeM4oFBb1mRVSDua1ulhfqvN ssh2 test CA
Loading