Schlüssel vorbereiten (auf beiden Seiten):
[ -f ~ /.ssh/id_ed25519 ] && [ -f ~ /.ssh/id_ed25519.pub ] || ssh-keygen -t ed25519
scp ~ /.ssh/id_ed25519.pub remote:from_remote_side/
Verschlüsselter io.ReadWriteCloser
, einfach:
// Generate (if not exists) and read ED25519 keys
identity , err := secureio . NewIdentity ( `/home/user/.ssh` )
// Read remote identity
remoteIdentity , err := secureio . NewRemoteIdentityFromPublicKey ( `/home/user/from_remote_side/id_ed25519.pub` )
// Create a connection
conn , err := net . Dial ( "udp" , "10.0.0.2:1234" )
// Create an encrypted connection (and exchange keys using ECDH and verify remote side by ED25519 signature).
session := identity . NewSession ( remoteIdentity , conn , nil , nil )
session . Start ( context . Background ())
// Use it!
// Write to it
_ , err = session . Write ( someData )
// Or/and read from it
_ , err = session . Read ( someData )
Richten Sie den Empfänger ein:
session . SetHandlerFuncs ( secureio . MessageTypeChannel ( 0 ), func ( payload [] byte ) {
fmt . Println ( "I received a payload:" , payload )
}, func ( err error ) {
panic ( err )
})
Senden Sie eine Nachricht synchron:
_ , err := session . WriteMessage ( secureio . MessageTypeChannel ( 0 ), payload )
ODER
Senden Sie eine Nachricht asynchron:
// Schedule the sending of the payload
sendInfo := session . WriteMessageAsync ( secureio . MessageTypeChannel ( 0 ), payload )
[.. your another stuff here if you want ..]
// Wait until the real sending
<- sendInfo . Done ()
// Here you get the error if any:
err := sendInfo. Err
// It's not necessary, but helps to reduce the pressure on GC (so to optimize CPU and RAM utilization)
sendInfo . Release ()
Ein MessageType für einen benutzerdefinierten Kanal kann über die Funktion MessageTypeChannel(channelID uint32)
erstellt werden. channelID
ist eine benutzerdefinierte Nummer zur Identifizierung des Flows (um einen Sender mit dem entsprechenden Empfänger auf der Remote-Seite zu verbinden). In den obigen Beispielen wurde 0
als channelID
-Wert verwendet, es könnte jedoch ein beliebiger Wert im Bereich sein: 0 <= x <= 2**31
.
Außerdem gibt es einen speziellen MessageType. MessageTypeReadWrite
wird standardmäßig für Read()
/ Write()
verwendet. Sie können diesen Fluss jedoch an einen benutzerdefinierten Handler umleiten.
SessionOptions.MaxPayloadSize
angepasst werden.SessionOptions.SendDelay
auf &[]time.Duration{0}[0]
setzen. Der Benchmark wurde mit Kommunikation über einen UNIX-Socket durchgeführt.
BenchmarkSessionWriteRead1-8 10000 118153 ns/op 0.01 MB/s 468 B/op 8 allocs/op
BenchmarkSessionWriteRead16-8 10000 118019 ns/op 0.14 MB/s 455 B/op 8 allocs/op
BenchmarkSessionWriteRead1024-8 9710 119238 ns/op 8.59 MB/s 441 B/op 8 allocs/op
BenchmarkSessionWriteRead32000-8 6980 173441 ns/op 184.50 MB/s 488 B/op 9 allocs/op
BenchmarkSessionWriteRead64000-8 3994 310038 ns/op 206.43 MB/s 629 B/op 9 allocs/op
BenchmarkSessionWriteMessageAsyncRead1-8 2285032 539 ns/op 1.86 MB/s 0 B/op 0 allocs/op
BenchmarkSessionWriteMessageAsyncRead16-8 2109264 572 ns/op 27.99 MB/s 2 B/op 0 allocs/op
BenchmarkSessionWriteMessageAsyncRead1024-8 480385 2404 ns/op 425.87 MB/s 15 B/op 0 allocs/op
BenchmarkSessionWriteMessageAsyncRead32000-8 30163 39131 ns/op 817.76 MB/s 162 B/op 5 allocs/op
BenchmarkSessionWriteMessageAsyncRead64000-8 15435 77898 ns/op 821.59 MB/s 317 B/op 10 allocs/op
Dieses Paket ist asynchron konzipiert, daher ist Write
im Grunde ein dummer Wrapper um den Code von WriteMessageAsync
. Um den Durchsatz zu erhöhen, werden alle in 50 Mikrosekunden gesammelten Nachrichten zu einer einzigen zusammengefasst, gesendet und dann wieder aufgeteilt. Dadurch können die Anzahl der Systemaufrufe und anderer Overheads reduziert werden. Um also etwa 1,86 MiB/s bei 1-Byte-Nachrichten zu erreichen, müssen Sie viele davon asynchron (voneinander) senden, damit sie beim Senden/Empfangen über die Backend-Verbindung zusammengeführt werden.
Auch bei diesen 800 MiB/s geht es eher um den Localhost-Fall. Und ein realistischerer Netzwerkfall (wenn wir MTU ~= 1400 haben) ist:
BenchmarkSessionWriteMessageAsyncRead1300_max1400-8 117862 10277 ns/op 126.49 MB/s 267 B/op 10 allocs/op
Die Authentifizierung der Gegenseite erfolgt durch eine ED25519-Signatur (der Schlüsselaustauschnachricht).
Der Schlüsselaustausch erfolgt über ECDH mit X25519. Wenn ein PSK festgelegt ist, wird dieser PSK mit einem konstanten Salt-Wert verkettet, mit blake3.Sum256
und sha3.Sum256
gehasht und zum XOR des (ausgetauschten) Schlüssels verwendet.
Der resultierende Wert wird als Verschlüsselungsschlüssel für XChaCha20 verwendet. Dieser Schlüssel wird im Code cipherKey
genannt.
Der Schlüssel (über ECDH empfangen) wird jede Minute aktualisiert. Daher wird auch der cipherKey
jede Minute aktualisiert.
Eine SessionID
wird bei den allerersten Schlüsselaustauschnachrichten ausgetauscht. SessionID
ist eine Kombination aus UnixNano (zum Zeitpunkt der Initialisierung der Sitzung) und einer zufälligen 64-Bit-Ganzzahl.
Außerdem beginnt jedes Paket mit einer eindeutigen (für eine Sitzung) Klartext- PacketID
(wenn PSK festgelegt ist, wird PacketID
tatsächlich mit einem Schlüssel verschlüsselt, der als Hash eines gesalzenen PSK abgeleitet ist).
Die Kombination aus PacketID
und SessionID
wird als IV/NONCE für XChaCha20 und cipherKey
als Schlüssel verwendet.
Erwähnenswert ist, dass PacketID
nur innerhalb einer Sitzung eindeutig sein soll. Es beginnt also einfach bei Null und erhöht sich dann mit jeder nächsten Nachricht um 1. Wenn daher die Systemuhr kaputt ist, wird die Eindeutigkeit von NONCE durch den 64-Bit-Zufallswert von SessionID
garantiert.
Die Nachrichtenauthentifizierung erfolgt über Poly1305. Als Schlüssel für Poly1305 wurde ein blake3.Sum256-Hash von:
PacketID
und cipherKey
XOR-verknüpft mit einem konstanten Wert. PacketID
soll nur zunehmen. Empfangene Paket-IDs werden in einem begrenzten Wertefenster gespeichert. Wenn ein Paket mit derselben PacketID
(wie bereits vorhanden) oder mit einer niedrigeren PackerID
(als dem minimal möglichen Wert im Fenster) empfangen wurde, wird das Paket einfach ignoriert.
Eine erfolgreiche Sitzung mit den Standardeinstellungen durchläuft Zustände/Phasen/Stufen:
Dies ist die Phase, in der alle Optionen analysiert und alle erforderlichen Goroutinen initialisiert werden.
Danach wird *Session
in den Zustand „Key Exchanging“ versetzt.
Zu diesem Zeitpunkt tauschen beide Parteien (die lokale und die entfernte) öffentliche ECDH-Schlüssel aus, um einen symmetrischen gemeinsamen Schlüssel zu erhalten (siehe „Sicherheitsdesign“).
Auch wenn eine Remote-Identität festgelegt ist, überprüfen wir, ob sie übereinstimmt, und ignorieren alle Schlüsselaustauschnachrichten mit anderen öffentlichen ED25519-Schlüsseln (nicht mit dem öffentlichen ECDH-Schlüssel verwechseln): ED25519-Schlüsselpaare sind für jede Partei statisch (und normalerweise vorab). definiert), während für jeden Schlüsselaustausch ECDH-Schlüsselpaare generiert werden.
Außerdem wird jeder Schlüsselaustauschschlüssel durch den öffentlichen Schlüssel (der mit der Nachricht übergeben wird) überprüft.
Mit den Standardeinstellungen sendet jede Partei auch Bestätigungsnachrichten, um zu überprüfen, ob die Nachrichten empfangen und wahrgenommen wurden. Und (mit den Standardeinstellungen) wartet jede Partei auf eine Bestätigungsnachricht von der Gegenseite (siehe auch SessionOptions.AnswersMode
).
Wenn hier alles erfolgreich ist, geht die *Session
in die Phase „Verhandlung“. Der Schlüsselaustauschprozess wird jedoch immer noch regelmäßig im Hintergrund durchgeführt.
In dieser Phase versuchen wir zu bestimmen, welche Paketgröße der zugrunde liegende io.Writer
verarbeiten kann. Also versuchen wir, Pakete in drei verschiedenen Größen zu verschicken und schauen, welches davon einen Hin- und Rückweg schafft. Anschließend den Vorgang in kürzeren Abständen wiederholen. Und so weiter bis zu 4 Mal.
Dieses Verhalten kann über SessionOptions.NegotiatorOptions.Enable
aktiviert oder deaktiviert werden.
Wenn dieser Vorgang abgeschlossen ist (oder deaktiviert ist), wird die *Session
in den Status „Established“ versetzt.
Dies ist der Zustand, in dem die *Session
normal funktioniert, sodass Sie über sie Nachrichten senden und empfangen können. Und Nachrichten, deren Senden vor Erreichen dieser Stufe versucht wurde, werden gesendet, sobald *Session
diese Stufe erreicht.
Closing
ist ein Übergangszustand, bevor es zu Closed
wird. Wenn der Status Closed
erreicht wird, bedeutet dies, dass die Sitzung tot ist und ihr nichts mehr passieren wird.
Async
nicht für synchrone Schreibvorgänge.notewakeup
anstelle von Cond.Wait