Initial commit
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
10
.idea/go.imports.xml
generated
Normal file
10
.idea/go.imports.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GoImports">
|
||||
<option name="excludedPackages">
|
||||
<array>
|
||||
<option value="golang.org/x/net/context" />
|
||||
</array>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
24
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
24
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="CssNonIntegerLengthInPixels" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="GoUnhandledErrorResult" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<methods>
|
||||
<method importPath="strings" receiver="*Builder" name="WriteByte" />
|
||||
<method importPath="bytes" receiver="*Buffer" name="WriteRune" />
|
||||
<method importPath="strings" receiver="*Builder" name="WriteString" />
|
||||
<method importPath="math/rand" receiver="*Rand" name="Read" />
|
||||
<method importPath="strings" receiver="*Builder" name="Write" />
|
||||
<method importPath="bytes" receiver="*Buffer" name="WriteByte" />
|
||||
<method importPath="bytes" receiver="*Buffer" name="Write" />
|
||||
<method importPath="strings" receiver="*Builder" name="WriteRune" />
|
||||
<method importPath="bytes" receiver="*Buffer" name="WriteString" />
|
||||
<method importPath="hash" receiver="Hash" name="Write" />
|
||||
<method importPath="encoding/json" receiver="*Encoder" name="Encode" />
|
||||
</methods>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="HtmlFormInputWithoutLabel" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="JSUnresolvedReference" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
9
.idea/lafsession.iml
generated
Normal file
9
.idea/lafsession.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/lafsession.iml" filepath="$PROJECT_DIR$/.idea/lafsession.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
87
README.md
Normal file
87
README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# lafsession
|
||||
|
||||
Go package: mutually authenticated AEAD session for opaque binary payloads (LAF envelopes) over a caller-provided transport.
|
||||
|
||||
## Algorithms
|
||||
|
||||
| Role | Algorithm |
|
||||
|------|-----------|
|
||||
| Identity | Ed25519 |
|
||||
| Ephemeral ECDH | X25519 |
|
||||
| KDF | HKDF-SHA256 |
|
||||
| Record AEAD | ChaCha20-Poly1305 |
|
||||
| Anti-replay | sliding bit window (default 1024) |
|
||||
|
||||
Protocol version `1`, suite `X25519 + ChaCha20-Poly1305`.
|
||||
|
||||
## Handshake
|
||||
|
||||
```
|
||||
ClientHello → ServerHello → ClientFinished → ServerFinished
|
||||
```
|
||||
|
||||
Transcript binds both ephemeral and identity public keys. HKDF info includes protocol label, version, suite, and transcript hash. Keys are derived at ClientFinished; ServerFinished is identity confirmation only.
|
||||
|
||||
## Record
|
||||
|
||||
```
|
||||
type (1) || seq (8 BE) || ChaCha20-Poly1305(ciphertext ‖ tag)
|
||||
```
|
||||
|
||||
Nonce = `0x00000000 ‖ seq`. AAD = `type ‖ seq`. Payload is opaque.
|
||||
|
||||
## Server
|
||||
|
||||
```go
|
||||
id, err := lafsession.GenerateIdentity()
|
||||
|
||||
s, err := lafsession.New(lafsession.Config{
|
||||
Identity: id,
|
||||
Role: lafsession.RoleServer,
|
||||
AcceptPeer: func(pub []byte) bool {
|
||||
return pinned[lafsession.FingerprintOf(pub)]
|
||||
},
|
||||
})
|
||||
|
||||
serverHello, err := s.HandleHandshake(clientHello)
|
||||
_, err = s.HandleHandshake(clientFinished)
|
||||
sf, err := s.SendFinished()
|
||||
|
||||
frame, err := s.Seal(lafBytes) // frame valid until next Seal/Close
|
||||
laf, err := s.Open(frame) // laf valid until next Open
|
||||
// zero-alloc when caller owns buffers:
|
||||
// frame, err = s.SealTo(dst[:0], lafBytes)
|
||||
// laf, err = s.OpenTo(out[:0], frame)
|
||||
closeFrame, err := s.Close()
|
||||
```
|
||||
|
||||
## Client
|
||||
|
||||
```go
|
||||
c, err := lafsession.New(lafsession.Config{
|
||||
Identity: clientID,
|
||||
Role: lafsession.RoleClient,
|
||||
AcceptPeer: func(pub []byte) bool {
|
||||
return lafsession.FingerprintOf(pub) == expectedServerFP
|
||||
},
|
||||
})
|
||||
|
||||
ch, err := c.StartHandshake()
|
||||
cf, err := c.HandleHandshake(serverHello)
|
||||
_, err = c.HandleHandshake(serverFinished)
|
||||
|
||||
frame, err := c.Seal(lafBytes)
|
||||
laf, err := c.Open(frame)
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
`ErrNotEstablished`, `ErrAlreadyEstablished`, `ErrIdentityRejected`, `ErrBadVersion`, `ErrBadCipher`, `ErrBadSignature`, `ErrBadTag`, `ErrReplay`, `ErrHandshakeState`, `ErrShortMessage`, `ErrClosed`.
|
||||
|
||||
## Module
|
||||
|
||||
```text
|
||||
wkgit.wk19.lol/wkgit/lafsession
|
||||
```
|
||||
|
||||
Replace the module path in `go.mod` when publishing under your own import path.
|
||||
17
errors.go
Normal file
17
errors.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package lafsession
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotEstablished = errors.New("lafsession: session not established")
|
||||
ErrAlreadyEstablished = errors.New("lafsession: session already established")
|
||||
ErrIdentityRejected = errors.New("lafsession: peer identity rejected")
|
||||
ErrBadVersion = errors.New("lafsession: unsupported protocol version")
|
||||
ErrBadCipher = errors.New("lafsession: unsupported cipher suite")
|
||||
ErrBadSignature = errors.New("lafsession: invalid identity signature")
|
||||
ErrBadTag = errors.New("lafsession: authentication failed")
|
||||
ErrReplay = errors.New("lafsession: replay or out-of-window sequence")
|
||||
ErrHandshakeState = errors.New("lafsession: unexpected handshake message")
|
||||
ErrShortMessage = errors.New("lafsession: message too short")
|
||||
ErrClosed = errors.New("lafsession: session closed")
|
||||
)
|
||||
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module wkgit.wk19.lol/wkgit/lafsession
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require golang.org/x/crypto v0.28.0
|
||||
|
||||
require golang.org/x/sys v0.26.0 // indirect
|
||||
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
295
handshake.go
Normal file
295
handshake.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package lafsession
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"hash"
|
||||
)
|
||||
|
||||
const (
|
||||
ProtocolVersion uint16 = 1
|
||||
CipherSuiteX25519ChaCha20Poly1305 uint16 = 1
|
||||
|
||||
msgClientHello byte = 1
|
||||
msgServerHello byte = 2
|
||||
msgClientFinished byte = 3
|
||||
msgServerFinished byte = 4
|
||||
|
||||
ephPubLen = 32
|
||||
idPubLen = 32
|
||||
sigLen = ed25519.SignatureSize
|
||||
|
||||
helloMsgLen = 1 + 2 + 2 + ephPubLen + idPubLen + sigLen
|
||||
|
||||
finishedMsgLen = 1 + sigLen
|
||||
|
||||
transcriptCap = helloMsgLen*2 + finishedMsgLen*2
|
||||
)
|
||||
|
||||
type HandshakeRole int
|
||||
|
||||
const (
|
||||
RoleClient HandshakeRole = iota
|
||||
RoleServer
|
||||
)
|
||||
|
||||
type peerAcceptFunc func(peerPub []byte) bool
|
||||
|
||||
type handshakeState struct {
|
||||
role HandshakeRole
|
||||
identity *Identity
|
||||
acceptPeer peerAcceptFunc
|
||||
|
||||
ephPriv *ecdh.PrivateKey
|
||||
ephPub [ephPubLen]byte
|
||||
|
||||
peerEphPub [ephPubLen]byte
|
||||
peerIDPub [idPubLen]byte
|
||||
hasPeer bool
|
||||
|
||||
transcript []byte
|
||||
hasher hash.Hash
|
||||
hashBuf [32]byte
|
||||
labelBuf [1 + 32]byte
|
||||
msgBuf []byte
|
||||
|
||||
version uint16
|
||||
suite uint16
|
||||
}
|
||||
|
||||
func newHandshake(role HandshakeRole, id *Identity, accept peerAcceptFunc) (*handshakeState, error) {
|
||||
curve := ecdh.X25519()
|
||||
eph, err := curve.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := &handshakeState{
|
||||
role: role,
|
||||
identity: id,
|
||||
acceptPeer: accept,
|
||||
ephPriv: eph,
|
||||
transcript: make([]byte, 0, transcriptCap),
|
||||
hasher: sha256.New(),
|
||||
msgBuf: make([]byte, 0, helloMsgLen),
|
||||
version: ProtocolVersion,
|
||||
suite: CipherSuiteX25519ChaCha20Poly1305,
|
||||
}
|
||||
copy(h.ephPub[:], eph.PublicKey().Bytes())
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *handshakeState) resetHasherFromTranscript() {
|
||||
h.hasher.Reset()
|
||||
_, _ = h.hasher.Write(h.transcript)
|
||||
}
|
||||
|
||||
func (h *handshakeState) transcriptSum(dst []byte) []byte {
|
||||
return h.hasher.Sum(h.hashBuf[:0])
|
||||
}
|
||||
|
||||
func (h *handshakeState) writeTranscript(p []byte) {
|
||||
h.transcript = append(h.transcript, p...)
|
||||
_, _ = h.hasher.Write(p)
|
||||
}
|
||||
|
||||
func (h *handshakeState) buildClientHello() ([]byte, error) {
|
||||
if h.role != RoleClient {
|
||||
return nil, ErrHandshakeState
|
||||
}
|
||||
msg := h.msgBuf[:0]
|
||||
msg = append(msg, msgClientHello)
|
||||
msg = binary.BigEndian.AppendUint16(msg, h.version)
|
||||
msg = binary.BigEndian.AppendUint16(msg, h.suite)
|
||||
msg = append(msg, h.ephPub[:]...)
|
||||
msg = append(msg, h.identity.Public...)
|
||||
|
||||
baseLen := len(msg)
|
||||
h.writeTranscript(msg)
|
||||
sum := h.transcriptSum(nil)
|
||||
sig := h.identity.Sign(sum)
|
||||
msg = append(msg, sig...)
|
||||
h.writeTranscript(sig)
|
||||
h.msgBuf = msg
|
||||
out := make([]byte, len(msg))
|
||||
copy(out, msg)
|
||||
_ = baseLen
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (h *handshakeState) processClientHello(msg []byte) error {
|
||||
if h.role != RoleServer {
|
||||
return ErrHandshakeState
|
||||
}
|
||||
if len(msg) < helloMsgLen {
|
||||
return ErrShortMessage
|
||||
}
|
||||
if msg[0] != msgClientHello {
|
||||
return ErrHandshakeState
|
||||
}
|
||||
ver := binary.BigEndian.Uint16(msg[1:3])
|
||||
suite := binary.BigEndian.Uint16(msg[3:5])
|
||||
if ver != ProtocolVersion {
|
||||
return ErrBadVersion
|
||||
}
|
||||
if suite != CipherSuiteX25519ChaCha20Poly1305 {
|
||||
return ErrBadCipher
|
||||
}
|
||||
off := 5
|
||||
peerEph := msg[off : off+ephPubLen]
|
||||
off += ephPubLen
|
||||
peerID := msg[off : off+idPubLen]
|
||||
off += idPubLen
|
||||
sig := msg[off : off+sigLen]
|
||||
|
||||
if h.acceptPeer != nil && !h.acceptPeer(peerID) {
|
||||
return ErrIdentityRejected
|
||||
}
|
||||
|
||||
base := msg[:5+ephPubLen+idPubLen]
|
||||
h.hasher.Reset()
|
||||
_, _ = h.hasher.Write(base)
|
||||
sum := h.transcriptSum(nil)
|
||||
if !VerifyPeer(peerID, sum, sig) {
|
||||
h.hasher.Reset()
|
||||
return ErrBadSignature
|
||||
}
|
||||
|
||||
h.version = ver
|
||||
h.suite = suite
|
||||
copy(h.peerEphPub[:], peerEph)
|
||||
copy(h.peerIDPub[:], peerID)
|
||||
h.hasPeer = true
|
||||
|
||||
h.transcript = append(h.transcript[:0], base...)
|
||||
h.transcript = append(h.transcript, sig...)
|
||||
_, _ = h.hasher.Write(sig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *handshakeState) buildServerHello() ([]byte, error) {
|
||||
if h.role != RoleServer {
|
||||
return nil, ErrHandshakeState
|
||||
}
|
||||
msg := h.msgBuf[:0]
|
||||
msg = append(msg, msgServerHello)
|
||||
msg = binary.BigEndian.AppendUint16(msg, h.version)
|
||||
msg = binary.BigEndian.AppendUint16(msg, h.suite)
|
||||
msg = append(msg, h.ephPub[:]...)
|
||||
msg = append(msg, h.identity.Public...)
|
||||
|
||||
h.writeTranscript(msg)
|
||||
sum := h.transcriptSum(nil)
|
||||
sig := h.identity.Sign(sum)
|
||||
msg = append(msg, sig...)
|
||||
h.writeTranscript(sig)
|
||||
h.msgBuf = msg
|
||||
|
||||
out := make([]byte, len(msg))
|
||||
copy(out, msg)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (h *handshakeState) processServerHello(msg []byte) error {
|
||||
if h.role != RoleClient {
|
||||
return ErrHandshakeState
|
||||
}
|
||||
if len(msg) < helloMsgLen {
|
||||
return ErrShortMessage
|
||||
}
|
||||
if msg[0] != msgServerHello {
|
||||
return ErrHandshakeState
|
||||
}
|
||||
ver := binary.BigEndian.Uint16(msg[1:3])
|
||||
suite := binary.BigEndian.Uint16(msg[3:5])
|
||||
if ver != ProtocolVersion {
|
||||
return ErrBadVersion
|
||||
}
|
||||
if suite != CipherSuiteX25519ChaCha20Poly1305 {
|
||||
return ErrBadCipher
|
||||
}
|
||||
off := 5
|
||||
peerEph := msg[off : off+ephPubLen]
|
||||
off += ephPubLen
|
||||
peerID := msg[off : off+idPubLen]
|
||||
off += idPubLen
|
||||
sig := msg[off : off+sigLen]
|
||||
|
||||
if h.acceptPeer != nil && !h.acceptPeer(peerID) {
|
||||
return ErrIdentityRejected
|
||||
}
|
||||
|
||||
base := msg[:5+ephPubLen+idPubLen]
|
||||
tLen := len(h.transcript)
|
||||
h.writeTranscript(base)
|
||||
sum := h.transcriptSum(nil)
|
||||
if !VerifyPeer(peerID, sum, sig) {
|
||||
h.transcript = h.transcript[:tLen]
|
||||
h.resetHasherFromTranscript()
|
||||
return ErrBadSignature
|
||||
}
|
||||
|
||||
copy(h.peerEphPub[:], peerEph)
|
||||
copy(h.peerIDPub[:], peerID)
|
||||
h.hasPeer = true
|
||||
h.writeTranscript(sig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *handshakeState) buildFinished(msgType byte) ([]byte, error) {
|
||||
sum := h.transcriptSum(nil)
|
||||
h.labelBuf[0] = msgType
|
||||
copy(h.labelBuf[1:], sum)
|
||||
sig := h.identity.Sign(h.labelBuf[:])
|
||||
msg := h.msgBuf[:0]
|
||||
msg = append(msg, msgType)
|
||||
msg = append(msg, sig...)
|
||||
h.writeTranscript(msg)
|
||||
h.msgBuf = msg
|
||||
|
||||
out := make([]byte, len(msg))
|
||||
copy(out, msg)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (h *handshakeState) processFinished(msg []byte, expectedType byte, peerID []byte) error {
|
||||
if len(msg) < finishedMsgLen {
|
||||
return ErrShortMessage
|
||||
}
|
||||
if msg[0] != expectedType {
|
||||
return ErrHandshakeState
|
||||
}
|
||||
sig := msg[1 : 1+sigLen]
|
||||
sum := h.transcriptSum(nil)
|
||||
h.labelBuf[0] = expectedType
|
||||
copy(h.labelBuf[1:], sum)
|
||||
if !VerifyPeer(peerID, h.labelBuf[:], sig) {
|
||||
return ErrBadSignature
|
||||
}
|
||||
h.writeTranscript(msg[:finishedMsgLen])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *handshakeState) deriveKeys() (*trafficKeys, error) {
|
||||
curve := ecdh.X25519()
|
||||
peerPub, err := curve.NewPublicKey(h.peerEphPub[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shared, err := h.ephPriv.ECDH(peerPub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zero(shared)
|
||||
sum := h.transcriptSum(nil)
|
||||
return deriveTrafficKeys(shared, sum, h.version, h.suite)
|
||||
}
|
||||
|
||||
func (h *handshakeState) peerIDSlice() []byte {
|
||||
if !h.hasPeer {
|
||||
return nil
|
||||
}
|
||||
return h.peerIDPub[:]
|
||||
}
|
||||
76
identity.go
Normal file
76
identity.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package lafsession
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
Private ed25519.PrivateKey
|
||||
Public ed25519.PublicKey
|
||||
}
|
||||
|
||||
func GenerateIdentity() (*Identity, error) {
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Identity{Private: priv, Public: pub}, nil
|
||||
}
|
||||
|
||||
func IdentityFromSeed(seed []byte) (*Identity, error) {
|
||||
if len(seed) != ed25519.SeedSize {
|
||||
return nil, fmt.Errorf("lafsession: seed must be %d bytes", ed25519.SeedSize)
|
||||
}
|
||||
priv := ed25519.NewKeyFromSeed(seed)
|
||||
return &Identity{Private: priv, Public: priv.Public().(ed25519.PublicKey)}, nil
|
||||
}
|
||||
|
||||
func (id *Identity) PublicBytes() []byte {
|
||||
return append([]byte(nil), id.Public...)
|
||||
}
|
||||
|
||||
func (id *Identity) Fingerprint() string {
|
||||
sum := sha256.Sum256(id.Public)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func (id *Identity) ShortFingerprint() string {
|
||||
sum := sha256.Sum256(id.Public)
|
||||
h := hex.EncodeToString(sum[:20])
|
||||
out := make([]byte, 0, 24+5)
|
||||
for i := 0; i < len(h); i += 4 {
|
||||
if i > 0 {
|
||||
out = append(out, ' ')
|
||||
}
|
||||
end := i + 4
|
||||
if end > len(h) {
|
||||
end = len(h)
|
||||
}
|
||||
out = append(out, h[i:end]...)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func (id *Identity) Sign(msg []byte) []byte {
|
||||
return ed25519.Sign(id.Private, msg)
|
||||
}
|
||||
|
||||
func (id *Identity) Verify(msg, sig []byte) bool {
|
||||
return ed25519.Verify(id.Public, msg, sig)
|
||||
}
|
||||
|
||||
func VerifyPeer(peerPub, msg, sig []byte) bool {
|
||||
if len(peerPub) != ed25519.PublicKeySize {
|
||||
return false
|
||||
}
|
||||
return ed25519.Verify(peerPub, msg, sig)
|
||||
}
|
||||
|
||||
func FingerprintOf(pub []byte) string {
|
||||
sum := sha256.Sum256(pub)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
61
keys.go
Normal file
61
keys.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package lafsession
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
const (
|
||||
keyLen = chacha20poly1305.KeySize
|
||||
nonceLen = chacha20poly1305.NonceSize
|
||||
tagLen = 16
|
||||
)
|
||||
|
||||
type trafficKeys struct {
|
||||
c2s [keyLen]byte
|
||||
s2c [keyLen]byte
|
||||
}
|
||||
|
||||
func deriveTrafficKeys(sharedSecret, transcriptHash []byte, version uint16, suite uint16) (*trafficKeys, error) {
|
||||
return deriveTrafficKeysImpl(sharedSecret, transcriptHash, version, suite)
|
||||
}
|
||||
|
||||
func deriveTrafficKeysImpl(sharedSecret, transcriptHash []byte, version uint16, suite uint16) (*trafficKeys, error) {
|
||||
info := buildHKDFInfo(version, suite, transcriptHash)
|
||||
r := hkdf.New(sha256.New, sharedSecret, nil, info)
|
||||
var tk trafficKeys
|
||||
if _, err := io.ReadFull(r, tk.c2s[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.ReadFull(r, tk.s2c[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tk, nil
|
||||
}
|
||||
|
||||
func buildHKDFInfo(version, suite uint16, transcriptHash []byte) []byte {
|
||||
const label = "lafsession ecdh-aead v1\x00"
|
||||
info := make([]byte, 0, len(label)+2+2+len(transcriptHash))
|
||||
info = append(info, label...)
|
||||
info = append(info, byte(version>>8), byte(version))
|
||||
info = append(info, byte(suite>>8), byte(suite))
|
||||
info = append(info, transcriptHash...)
|
||||
return info
|
||||
}
|
||||
|
||||
func zero(b []byte) {
|
||||
for i := range b {
|
||||
b[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (tk *trafficKeys) wipe() {
|
||||
if tk == nil {
|
||||
return
|
||||
}
|
||||
zero(tk.c2s[:])
|
||||
zero(tk.s2c[:])
|
||||
}
|
||||
73
record.go
Normal file
73
record.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package lafsession
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
const (
|
||||
recordHeaderLen = 1 + 8
|
||||
|
||||
recordTypeData byte = 0x10
|
||||
recordTypeClose byte = 0x11
|
||||
)
|
||||
|
||||
func newAEAD(key []byte) (cipher.AEAD, error) {
|
||||
return chacha20poly1305.New(key)
|
||||
}
|
||||
|
||||
func sealRecord(aead cipher.AEAD, seq uint64, recType byte, plaintext, dst []byte, nonce *[nonceLen]byte, aad *[recordHeaderLen]byte) []byte {
|
||||
aad[0] = recType
|
||||
binary.BigEndian.PutUint64(aad[1:], seq)
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
nonce[i] = 0
|
||||
}
|
||||
binary.BigEndian.PutUint64(nonce[4:], seq)
|
||||
|
||||
need := recordHeaderLen + len(plaintext) + aead.Overhead()
|
||||
if cap(dst) < need {
|
||||
dst = make([]byte, need)
|
||||
} else {
|
||||
dst = dst[:need]
|
||||
}
|
||||
dst[0] = recType
|
||||
binary.BigEndian.PutUint64(dst[1:9], seq)
|
||||
_ = aead.Seal(dst[recordHeaderLen:recordHeaderLen], nonce[:], plaintext, aad[:])
|
||||
return dst
|
||||
}
|
||||
|
||||
// openRecord decrypts a full frame. Returns record type and plaintext (reusing dst when possible).
|
||||
func openRecord(aead cipher.AEAD, frame, dst []byte, nonce *[nonceLen]byte, aad *[recordHeaderLen]byte) (recType byte, plaintext []byte, err error) {
|
||||
if len(frame) < recordHeaderLen+aead.Overhead() {
|
||||
return 0, nil, ErrShortMessage
|
||||
}
|
||||
recType = frame[0]
|
||||
seq := binary.BigEndian.Uint64(frame[1:9])
|
||||
|
||||
aad[0] = recType
|
||||
binary.BigEndian.PutUint64(aad[1:], seq)
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
nonce[i] = 0
|
||||
}
|
||||
binary.BigEndian.PutUint64(nonce[4:], seq)
|
||||
|
||||
ct := frame[recordHeaderLen:]
|
||||
ptLen := len(ct) - aead.Overhead()
|
||||
if ptLen < 0 {
|
||||
return 0, nil, ErrShortMessage
|
||||
}
|
||||
if cap(dst) < ptLen {
|
||||
dst = make([]byte, ptLen)
|
||||
} else {
|
||||
dst = dst[:ptLen]
|
||||
}
|
||||
out, err := aead.Open(dst[:0], nonce[:], ct, aad[:])
|
||||
if err != nil {
|
||||
return 0, nil, ErrBadTag
|
||||
}
|
||||
return recType, out, nil
|
||||
}
|
||||
79
replay.go
Normal file
79
replay.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package lafsession
|
||||
|
||||
type replayWindow struct {
|
||||
maxSeq uint64
|
||||
size uint64
|
||||
bits []uint64
|
||||
}
|
||||
|
||||
func newReplayWindow(size uint64) *replayWindow {
|
||||
if size < 64 {
|
||||
size = 64
|
||||
}
|
||||
words := (size + 63) / 64
|
||||
return &replayWindow{
|
||||
size: size,
|
||||
bits: make([]uint64, words),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *replayWindow) accept(seq uint64) bool {
|
||||
if seq+w.size <= w.maxSeq {
|
||||
return false
|
||||
}
|
||||
if seq > w.maxSeq {
|
||||
shift := seq - w.maxSeq
|
||||
if shift >= w.size {
|
||||
for i := range w.bits {
|
||||
w.bits[i] = 0
|
||||
}
|
||||
} else {
|
||||
w.shift(shift)
|
||||
}
|
||||
w.maxSeq = seq
|
||||
}
|
||||
idx := w.maxSeq - seq
|
||||
word := idx / 64
|
||||
bit := idx % 64
|
||||
if word >= uint64(len(w.bits)) {
|
||||
return false
|
||||
}
|
||||
mask := uint64(1) << bit
|
||||
if w.bits[word]&mask != 0 {
|
||||
return false
|
||||
}
|
||||
w.bits[word] |= mask
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *replayWindow) shift(n uint64) {
|
||||
words := n / 64
|
||||
bits := n % 64
|
||||
if words > 0 {
|
||||
copy(w.bits[words:], w.bits)
|
||||
for i := uint64(0); i < words && i < uint64(len(w.bits)); i++ {
|
||||
w.bits[i] = 0
|
||||
}
|
||||
for i := uint64(len(w.bits)); i > words; i-- {
|
||||
}
|
||||
if words < uint64(len(w.bits)) {
|
||||
for i := 0; i < int(words); i++ {
|
||||
w.bits[i] = 0
|
||||
}
|
||||
} else {
|
||||
for i := range w.bits {
|
||||
w.bits[i] = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if bits == 0 {
|
||||
return
|
||||
}
|
||||
var carry uint64
|
||||
for i := len(w.bits) - 1; i >= 0; i-- {
|
||||
newCarry := w.bits[i] >> (64 - bits)
|
||||
w.bits[i] = (w.bits[i] << bits) | carry
|
||||
carry = newCarry
|
||||
}
|
||||
}
|
||||
335
session.go
Normal file
335
session.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package lafsession
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Identity *Identity
|
||||
|
||||
Role HandshakeRole
|
||||
|
||||
AcceptPeer func(peerPub []byte) bool
|
||||
|
||||
ReplayWindowSize uint64
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
mu sync.Mutex
|
||||
|
||||
cfg Config
|
||||
hs *handshakeState
|
||||
|
||||
keys *trafficKeys
|
||||
sendAEAD cipher.AEAD
|
||||
recvAEAD cipher.AEAD
|
||||
|
||||
sendSeq uint64
|
||||
recvWin *replayWindow
|
||||
|
||||
sealBuf []byte
|
||||
openBuf []byte
|
||||
nonce [nonceLen]byte
|
||||
aad [recordHeaderLen]byte
|
||||
|
||||
established bool
|
||||
closed bool
|
||||
pendingServerFinished bool
|
||||
}
|
||||
|
||||
func New(cfg Config) (*Session, error) {
|
||||
if cfg.Identity == nil {
|
||||
return nil, ErrIdentityRejected
|
||||
}
|
||||
if cfg.ReplayWindowSize == 0 {
|
||||
cfg.ReplayWindowSize = 1024
|
||||
}
|
||||
hs, err := newHandshake(cfg.Role, cfg.Identity, cfg.AcceptPeer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Session{
|
||||
cfg: cfg,
|
||||
hs: hs,
|
||||
recvWin: newReplayWindow(cfg.ReplayWindowSize),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Session) StartHandshake() ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if s.established {
|
||||
return nil, ErrAlreadyEstablished
|
||||
}
|
||||
if s.cfg.Role != RoleClient {
|
||||
return nil, ErrHandshakeState
|
||||
}
|
||||
return s.hs.buildClientHello()
|
||||
}
|
||||
|
||||
func (s *Session) HandleHandshake(msg []byte) (response []byte, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if s.established {
|
||||
return nil, ErrAlreadyEstablished
|
||||
}
|
||||
if len(msg) < 1 {
|
||||
return nil, ErrShortMessage
|
||||
}
|
||||
|
||||
switch s.cfg.Role {
|
||||
case RoleServer:
|
||||
return s.handleServer(msg)
|
||||
case RoleClient:
|
||||
return s.handleClient(msg)
|
||||
default:
|
||||
return nil, ErrHandshakeState
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) handleServer(msg []byte) ([]byte, error) {
|
||||
switch msg[0] {
|
||||
case msgClientHello:
|
||||
if err := s.hs.processClientHello(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.hs.buildServerHello()
|
||||
case msgClientFinished:
|
||||
if err := s.hs.processFinished(msg, msgClientFinished, s.hs.peerIDSlice()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys, err := s.hs.deriveKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.installKeys(keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.pendingServerFinished = true
|
||||
s.established = true
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, ErrHandshakeState
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) handleClient(msg []byte) ([]byte, error) {
|
||||
switch msg[0] {
|
||||
case msgServerHello:
|
||||
if err := s.hs.processServerHello(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cf, err := s.hs.buildFinished(msgClientFinished)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys, err := s.hs.deriveKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.installKeys(keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cf, nil
|
||||
case msgServerFinished:
|
||||
if err := s.hs.processFinished(msg, msgServerFinished, s.hs.peerIDSlice()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.keys == nil {
|
||||
return nil, ErrHandshakeState
|
||||
}
|
||||
s.established = true
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, ErrHandshakeState
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) SendFinished() ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if s.cfg.Role != RoleServer || !s.pendingServerFinished {
|
||||
return nil, ErrHandshakeState
|
||||
}
|
||||
msg, err := s.hs.buildFinished(msgServerFinished)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.pendingServerFinished = false
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (s *Session) installKeys(keys *trafficKeys) error {
|
||||
var sendKey, recvKey []byte
|
||||
if s.cfg.Role == RoleClient {
|
||||
sendKey, recvKey = keys.c2s[:], keys.s2c[:]
|
||||
} else {
|
||||
sendKey, recvKey = keys.s2c[:], keys.c2s[:]
|
||||
}
|
||||
sa, err := newAEAD(sendKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ra, err := newAEAD(recvKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.keys != nil {
|
||||
s.keys.wipe()
|
||||
}
|
||||
s.keys = keys
|
||||
s.sendAEAD = sa
|
||||
s.recvAEAD = ra
|
||||
s.sendSeq = 0
|
||||
s.recvWin = newReplayWindow(s.cfg.ReplayWindowSize)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Established() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.established && !s.closed
|
||||
}
|
||||
|
||||
func (s *Session) PeerPublicKey() []byte {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.hs == nil || !s.hs.hasPeer {
|
||||
return nil
|
||||
}
|
||||
out := make([]byte, idPubLen)
|
||||
copy(out, s.hs.peerIDPub[:])
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Session) Seal(lafPayload []byte) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if !s.established || s.sendAEAD == nil {
|
||||
return nil, ErrNotEstablished
|
||||
}
|
||||
seq := s.sendSeq
|
||||
s.sendSeq++
|
||||
s.sealBuf = sealRecord(s.sendAEAD, seq, recordTypeData, lafPayload, s.sealBuf, &s.nonce, &s.aad)
|
||||
return s.sealBuf, nil
|
||||
}
|
||||
|
||||
func (s *Session) SealTo(dst, lafPayload []byte) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if !s.established || s.sendAEAD == nil {
|
||||
return nil, ErrNotEstablished
|
||||
}
|
||||
seq := s.sendSeq
|
||||
s.sendSeq++
|
||||
return sealRecord(s.sendAEAD, seq, recordTypeData, lafPayload, dst, &s.nonce, &s.aad), nil
|
||||
}
|
||||
|
||||
func (s *Session) Open(frame []byte) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if !s.established || s.recvAEAD == nil {
|
||||
return nil, ErrNotEstablished
|
||||
}
|
||||
if len(frame) < recordHeaderLen+tagLen {
|
||||
return nil, ErrShortMessage
|
||||
}
|
||||
seq := binary.BigEndian.Uint64(frame[1:9])
|
||||
if !s.recvWin.accept(seq) {
|
||||
return nil, ErrReplay
|
||||
}
|
||||
recType, pt, err := openRecord(s.recvAEAD, frame, s.openBuf, &s.nonce, &s.aad)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.openBuf = pt
|
||||
switch recType {
|
||||
case recordTypeData:
|
||||
return pt, nil
|
||||
case recordTypeClose:
|
||||
s.closed = true
|
||||
s.wipeKeys()
|
||||
return nil, ErrClosed
|
||||
default:
|
||||
return nil, ErrBadTag
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) OpenTo(dst, frame []byte) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if !s.established || s.recvAEAD == nil {
|
||||
return nil, ErrNotEstablished
|
||||
}
|
||||
if len(frame) < recordHeaderLen+tagLen {
|
||||
return nil, ErrShortMessage
|
||||
}
|
||||
seq := binary.BigEndian.Uint64(frame[1:9])
|
||||
if !s.recvWin.accept(seq) {
|
||||
return nil, ErrReplay
|
||||
}
|
||||
recType, pt, err := openRecord(s.recvAEAD, frame, dst, &s.nonce, &s.aad)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch recType {
|
||||
case recordTypeData:
|
||||
return pt, nil
|
||||
case recordTypeClose:
|
||||
s.closed = true
|
||||
s.wipeKeys()
|
||||
return nil, ErrClosed
|
||||
default:
|
||||
return nil, ErrBadTag
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) Close() (frame []byte, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return nil, nil
|
||||
}
|
||||
if s.established && s.sendAEAD != nil {
|
||||
seq := s.sendSeq
|
||||
s.sendSeq++
|
||||
s.sealBuf = sealRecord(s.sendAEAD, seq, recordTypeClose, nil, s.sealBuf, &s.nonce, &s.aad)
|
||||
frame = s.sealBuf
|
||||
}
|
||||
s.closed = true
|
||||
s.established = false
|
||||
s.wipeKeys()
|
||||
return frame, err
|
||||
}
|
||||
|
||||
func (s *Session) wipeKeys() {
|
||||
if s.keys != nil {
|
||||
s.keys.wipe()
|
||||
s.keys = nil
|
||||
}
|
||||
s.sendAEAD = nil
|
||||
s.recvAEAD = nil
|
||||
}
|
||||
359
session_bench_test.go
Normal file
359
session_bench_test.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package lafsession
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupPair(b *testing.B) (cli, srv *Session) {
|
||||
b.Helper()
|
||||
serverID, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
clientID, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
clientPub := clientID.PublicBytes()
|
||||
serverPub := serverID.PublicBytes()
|
||||
srv, err = New(Config{
|
||||
Identity: serverID,
|
||||
Role: RoleServer,
|
||||
AcceptPeer: func(pub []byte) bool {
|
||||
return bytesEqual32(pub, clientPub)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
cli, err = New(Config{
|
||||
Identity: clientID,
|
||||
Role: RoleClient,
|
||||
AcceptPeer: func(pub []byte) bool {
|
||||
return bytesEqual32(pub, serverPub)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
ch, err := cli.StartHandshake()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
sh, err := srv.HandleHandshake(ch)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
cf, err := cli.HandleHandshake(sh)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if _, err = srv.HandleHandshake(cf); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
sf, err := srv.SendFinished()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if _, err = cli.HandleHandshake(sf); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if !cli.Established() || !srv.Established() {
|
||||
b.Fatal("not established")
|
||||
}
|
||||
return cli, srv
|
||||
}
|
||||
|
||||
func BenchmarkHandshake(b *testing.B) {
|
||||
serverID, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
clientID, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
clientPub := clientID.PublicBytes()
|
||||
serverPub := serverID.PublicBytes()
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
srv, err := New(Config{
|
||||
Identity: serverID,
|
||||
Role: RoleServer,
|
||||
AcceptPeer: func(pub []byte) bool { return bytesEqual32(pub, clientPub) },
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
cli, err := New(Config{
|
||||
Identity: clientID,
|
||||
Role: RoleClient,
|
||||
AcceptPeer: func(pub []byte) bool { return bytesEqual32(pub, serverPub) },
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
ch, err := cli.StartHandshake()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
sh, err := srv.HandleHandshake(ch)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
cf, err := cli.HandleHandshake(sh)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if _, err = srv.HandleHandshake(cf); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
sf, err := srv.SendFinished()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if _, err = cli.HandleHandshake(sf); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSeal(b *testing.B) {
|
||||
cli, _ := setupPair(b)
|
||||
payload := make([]byte, 256)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i)
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := cli.Seal(payload); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOpen(b *testing.B) {
|
||||
cli, srv := setupPair(b)
|
||||
payload := make([]byte, 256)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i)
|
||||
}
|
||||
|
||||
frames := make([][]byte, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
f, err := cli.Seal(payload)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
frames[i] = append([]byte(nil), f...)
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := srv.Open(frames[i]); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSealOpenRoundTrip(b *testing.B) {
|
||||
cli, srv := setupPair(b)
|
||||
payload := make([]byte, 256)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i)
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
frame, err := cli.Seal(payload)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if _, err := srv.Open(frame); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSealOpenSizes(b *testing.B) {
|
||||
sizes := []int{64, 256, 1024, 4096}
|
||||
for _, sz := range sizes {
|
||||
sz := sz
|
||||
b.Run(itoa(sz), func(b *testing.B) {
|
||||
cli, srv := setupPair(b)
|
||||
payload := make([]byte, sz)
|
||||
b.SetBytes(int64(sz))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
frame, err := cli.Seal(payload)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if _, err := srv.Open(frame); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReplayWindow(b *testing.B) {
|
||||
w := newReplayWindow(1024)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !w.accept(uint64(i)) {
|
||||
b.Fatal("unexpected reject")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReplayWindowWithGaps(b *testing.B) {
|
||||
w := newReplayWindow(1024)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
seq := uint64(i)
|
||||
if i%2 == 0 {
|
||||
if !w.accept(seq + 1) {
|
||||
}
|
||||
}
|
||||
_ = w.accept(seq)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFingerprint(b *testing.B) {
|
||||
id, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
pub := id.PublicBytes()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = FingerprintOf(pub)
|
||||
}
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var buf [16]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
|
||||
func BenchmarkSealToOpenTo(b *testing.B) {
|
||||
cli, srv := setupPair(b)
|
||||
payload := make([]byte, 256)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i)
|
||||
}
|
||||
sealDst := make([]byte, 0, recordHeaderLen+len(payload)+tagLen)
|
||||
openDst := make([]byte, 0, len(payload))
|
||||
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
frame, err := cli.SealTo(sealDst[:0], payload)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
pt, err := srv.OpenTo(openDst[:0], frame)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if len(pt) != len(payload) {
|
||||
b.Fatal("len")
|
||||
}
|
||||
_ = frame
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSealSteady(b *testing.B) {
|
||||
cli, _ := setupPair(b)
|
||||
payload := make([]byte, 256)
|
||||
if _, err := cli.Seal(payload); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := cli.Seal(payload); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOpenSteady(b *testing.B) {
|
||||
cli, srv := setupPair(b)
|
||||
payload := make([]byte, 256)
|
||||
frames := make([][]byte, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
f, err := cli.Seal(payload)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
frames[i] = append([]byte(nil), f...)
|
||||
}
|
||||
if _, err := srv.Open(frames[0]); err != nil && err != ErrReplay {
|
||||
}
|
||||
cli, srv = setupPair(b)
|
||||
frames = make([][]byte, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
f, err := cli.Seal(payload)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
frames[i] = append([]byte(nil), f...)
|
||||
}
|
||||
if _, err := srv.Open(append([]byte(nil), frames[0]...)); err == nil {
|
||||
}
|
||||
cli, srv = setupPair(b)
|
||||
frames = make([][]byte, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
f, _ := cli.Seal(payload)
|
||||
frames[i] = append([]byte(nil), f...)
|
||||
}
|
||||
if _, err := srv.Open(frames[0]); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 1; i < b.N; i++ {
|
||||
if _, err := srv.Open(frames[i]); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func bytesEqual32(a, b []byte) bool {
|
||||
if len(a) != 32 || len(b) != 32 {
|
||||
return false
|
||||
}
|
||||
var v byte
|
||||
for i := 0; i < 32; i++ {
|
||||
v |= a[i] ^ b[i]
|
||||
}
|
||||
return v == 0
|
||||
}
|
||||
139
session_test.go
Normal file
139
session_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package lafsession
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandshakeAndSealOpen(t *testing.T) {
|
||||
serverID, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientID, err := GenerateIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
serverPin := make(map[string]bool)
|
||||
serverPin[FingerprintOf(clientID.PublicBytes())] = true
|
||||
clientPin := make(map[string]bool)
|
||||
clientPin[FingerprintOf(serverID.PublicBytes())] = true
|
||||
|
||||
srv, err := New(Config{
|
||||
Identity: serverID,
|
||||
Role: RoleServer,
|
||||
AcceptPeer: func(pub []byte) bool {
|
||||
return serverPin[FingerprintOf(pub)]
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cli, err := New(Config{
|
||||
Identity: clientID,
|
||||
Role: RoleClient,
|
||||
AcceptPeer: func(pub []byte) bool {
|
||||
return clientPin[FingerprintOf(pub)]
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ch, err := cli.StartHandshake()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sh, err := srv.HandleHandshake(ch)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sh == nil {
|
||||
t.Fatal("expected ServerHello")
|
||||
}
|
||||
|
||||
cf, err := cli.HandleHandshake(sh)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cf == nil {
|
||||
t.Fatal("expected ClientFinished")
|
||||
}
|
||||
|
||||
_, err = srv.HandleHandshake(cf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !srv.Established() {
|
||||
t.Fatal("server should be established after ClientFinished")
|
||||
}
|
||||
|
||||
sf, err := srv.SendFinished()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = cli.HandleHandshake(sf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !cli.Established() {
|
||||
t.Fatal("client should be established after ServerFinished")
|
||||
}
|
||||
|
||||
laf := []byte{0x05, 0x80, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0xde, 0xad, 0xbe, 0xef}
|
||||
|
||||
frame, err := cli.Seal(laf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := srv.Open(frame)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, laf) {
|
||||
t.Fatalf("payload mismatch: got %x want %x", got, laf)
|
||||
}
|
||||
|
||||
frame2, err := srv.Seal(laf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got2, err := cli.Open(frame2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got2, laf) {
|
||||
t.Fatalf("payload mismatch c2s")
|
||||
}
|
||||
|
||||
_, err = srv.Open(frame)
|
||||
if err != ErrReplay {
|
||||
t.Fatalf("expected ErrReplay, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectUnknownPeer(t *testing.T) {
|
||||
serverID, _ := GenerateIdentity()
|
||||
clientID, _ := GenerateIdentity()
|
||||
|
||||
srv, _ := New(Config{
|
||||
Identity: serverID,
|
||||
Role: RoleServer,
|
||||
AcceptPeer: func(pub []byte) bool {
|
||||
return false
|
||||
},
|
||||
})
|
||||
cli, _ := New(Config{
|
||||
Identity: clientID,
|
||||
Role: RoleClient,
|
||||
AcceptPeer: func(pub []byte) bool { return true },
|
||||
})
|
||||
|
||||
ch, _ := cli.StartHandshake()
|
||||
_, err := srv.HandleHandshake(ch)
|
||||
if err != ErrIdentityRejected {
|
||||
t.Fatalf("expected ErrIdentityRejected, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user