33
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
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
# ---> Go
|
|
||||||
# If you prefer the allow list template instead of the deny list, see community template:
|
# 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
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
#
|
#
|
||||||
@@ -24,4 +23,3 @@ go.work.sum
|
|||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.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>
|
||||||
85
README.md
85
README.md
@@ -1,2 +1,87 @@
|
|||||||
# lafsession
|
# 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
|
||||||
|
github.com/example/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 github.com/example/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=
|
||||||
304
handshake.go
Normal file
304
handshake.go
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
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 // 64
|
||||||
|
|
||||||
|
// type|ver|suite|eph|id|sig
|
||||||
|
helloMsgLen = 1 + 2 + 2 + ephPubLen + idPubLen + sigLen // 133
|
||||||
|
// type|sig
|
||||||
|
finishedMsgLen = 1 + sigLen // 65
|
||||||
|
|
||||||
|
// CH+SH+CF+SF upper bound
|
||||||
|
transcriptCap = helloMsgLen*2 + finishedMsgLen*2 // 396
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandshakeRole is which side we act as.
|
||||||
|
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 // running SHA-256 over transcript; kept in sync with transcript
|
||||||
|
hashBuf [32]byte
|
||||||
|
labelBuf [1 + 32]byte // finished label: type || transcriptHash
|
||||||
|
msgBuf []byte // reusable outbound message buffer
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// Sum into hashBuf then copy to dst if needed; Sum allocates if dst too small
|
||||||
|
return h.hasher.Sum(h.hashBuf[:0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handshakeState) writeTranscript(p []byte) {
|
||||||
|
h.transcript = append(h.transcript, p...)
|
||||||
|
_, _ = h.hasher.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildClientHello: type | ver | suite | ephPub | idPub | sig
|
||||||
|
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
|
||||||
|
// return a copy so caller's network write is independent of msgBuf reuse
|
||||||
|
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]
|
||||||
|
// Verify against hash(base) without permanently writing sig yet
|
||||||
|
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
|
||||||
|
|
||||||
|
// hasher already holds base from verify; only append sig
|
||||||
|
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]
|
||||||
|
// Temporarily extend transcript for verify; roll back on failure
|
||||||
|
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[:]
|
||||||
|
}
|
||||||
86
identity.go
Normal file
86
identity.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package lafsession
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Identity is a long-term Ed25519 key pair used for mutual authentication.
|
||||||
|
type Identity struct {
|
||||||
|
Private ed25519.PrivateKey
|
||||||
|
Public ed25519.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateIdentity creates a new random identity key pair.
|
||||||
|
func GenerateIdentity() (*Identity, error) {
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Identity{Private: priv, Public: pub}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdentityFromSeed derives an identity from a 32-byte seed (deterministic).
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicBytes returns the 32-byte public key.
|
||||||
|
func (id *Identity) PublicBytes() []byte {
|
||||||
|
return append([]byte(nil), id.Public...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fingerprint returns a SHA-256 hex fingerprint of the public key (full 32 bytes hex).
|
||||||
|
func (id *Identity) Fingerprint() string {
|
||||||
|
sum := sha256.Sum256(id.Public)
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortFingerprint returns the first 20 bytes of SHA-256 as spaced hex groups (display).
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs a transcript with the identity private key.
|
||||||
|
func (id *Identity) Sign(msg []byte) []byte {
|
||||||
|
return ed25519.Sign(id.Private, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks a signature under this identity public key.
|
||||||
|
func (id *Identity) Verify(msg, sig []byte) bool {
|
||||||
|
return ed25519.Verify(id.Public, msg, sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPeer checks a signature under an arbitrary peer public key.
|
||||||
|
func VerifyPeer(peerPub, msg, sig []byte) bool {
|
||||||
|
if len(peerPub) != ed25519.PublicKeySize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ed25519.Verify(ed25519.PublicKey(peerPub), msg, sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FingerprintOf returns SHA-256 hex of a raw public key.
|
||||||
|
func FingerprintOf(pub []byte) string {
|
||||||
|
sum := sha256.Sum256(pub)
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
62
keys.go
Normal file
62
keys.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package lafsession
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyLen = chacha20poly1305.KeySize // 32
|
||||||
|
nonceLen = chacha20poly1305.NonceSize // 12
|
||||||
|
tagLen = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// trafficKeys holds direction-specific AEAD keys after handshake.
|
||||||
|
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[:])
|
||||||
|
}
|
||||||
76
record.go
Normal file
76
record.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package lafsession
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
recordHeaderLen = 1 + 8 // type + seq
|
||||||
|
|
||||||
|
recordTypeData byte = 0x10
|
||||||
|
recordTypeClose byte = 0x11
|
||||||
|
)
|
||||||
|
|
||||||
|
func newAEAD(key []byte) (cipher.AEAD, error) {
|
||||||
|
return chacha20poly1305.New(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sealRecord builds: type || seq(8 BE) || ciphertext||tag
|
||||||
|
// Nonce = 0x00000000 || seq. AAD = type || seq.
|
||||||
|
// dst is reused as output buffer when capacity is sufficient.
|
||||||
|
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
|
||||||
|
}
|
||||||
85
replay.go
Normal file
85
replay.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package lafsession
|
||||||
|
|
||||||
|
// replayWindow is a sliding bit-window anti-replay tracker for a single direction.
|
||||||
|
type replayWindow struct {
|
||||||
|
maxSeq uint64
|
||||||
|
size uint64 // bits
|
||||||
|
bits []uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReplayWindow(size uint64) *replayWindow {
|
||||||
|
if size < 64 {
|
||||||
|
size = 64
|
||||||
|
}
|
||||||
|
words := (size + 63) / 64
|
||||||
|
return &replayWindow{
|
||||||
|
size: size,
|
||||||
|
bits: make([]uint64, words),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// accept returns true if seq is new and within window; marks it seen.
|
||||||
|
func (w *replayWindow) accept(seq uint64) bool {
|
||||||
|
if seq+w.size <= w.maxSeq {
|
||||||
|
// too old
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if seq > w.maxSeq {
|
||||||
|
shift := seq - w.maxSeq
|
||||||
|
if shift >= w.size {
|
||||||
|
// clear entire window
|
||||||
|
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 // already seen
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// zero the tail that was shifted out conceptually
|
||||||
|
for i := uint64(len(w.bits)); i > words; i-- {
|
||||||
|
// already copied
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
373
session.go
Normal file
373
session.go
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
package lafsession
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/binary"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config configures a Session before handshake.
|
||||||
|
type Config struct {
|
||||||
|
// Identity is this side's long-term key. Required.
|
||||||
|
Identity *Identity
|
||||||
|
|
||||||
|
// Role is RoleServer or RoleClient.
|
||||||
|
Role HandshakeRole
|
||||||
|
|
||||||
|
// AcceptPeer is called with the peer's 32-byte identity public key during
|
||||||
|
// handshake. Return false to reject (ErrIdentityRejected).
|
||||||
|
// If nil, any peer identity is accepted (not recommended for production).
|
||||||
|
AcceptPeer func(peerPub []byte) bool
|
||||||
|
|
||||||
|
// ReplayWindowSize is the anti-replay window in packets (default 1024).
|
||||||
|
ReplayWindowSize uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session is a mutually authenticated AEAD channel that carries opaque LAF payloads.
|
||||||
|
//
|
||||||
|
// Typical server flow:
|
||||||
|
//
|
||||||
|
// s, _ := New(Config{Identity: serverID, Role: RoleServer, AcceptPeer: pinCheck})
|
||||||
|
// // read clientHello from network
|
||||||
|
// serverHello, _ := s.HandleHandshake(clientHello)
|
||||||
|
// // write serverHello
|
||||||
|
// // read clientFinished
|
||||||
|
// _, _ = s.HandleHandshake(clientFinished)
|
||||||
|
// // optional: write serverFinished
|
||||||
|
// sf, _ := s.SendFinished()
|
||||||
|
// // write sf
|
||||||
|
// // now Established()
|
||||||
|
// frame, _ := s.Seal(lafBytes)
|
||||||
|
// laf, _ := s.Open(frame)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a Session in pre-handshake state.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartHandshake is called by the client to produce ClientHello.
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleHandshake processes one inbound handshake message and returns an
|
||||||
|
// optional outbound response (may be nil).
|
||||||
|
//
|
||||||
|
// Server: ClientHello → ServerHello; ClientFinished → (nil, establishes keys).
|
||||||
|
// Client: ServerHello → ClientFinished; ServerFinished → (nil, establishes keys).
|
||||||
|
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 from transcript through ClientFinished — same point the server uses.
|
||||||
|
keys, err := s.hs.deriveKeys()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.installKeys(keys); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Established only after ServerFinished confirms the peer finished.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendFinished returns ServerFinished (server only, after ClientFinished processed).
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Established reports whether application Seal/Open are allowed.
|
||||||
|
func (s *Session) Established() bool {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.established && !s.closed
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerPublicKey returns the peer identity public key after it has been received.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open decrypts and verifies a record frame.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close sends an authenticated close record (if established) and wipes keys.
|
||||||
|
// Write the returned frame to the peer when non-nil.
|
||||||
|
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
|
||||||
|
}
|
||||||
384
session_bench_test.go
Normal file
384
session_bench_test.go
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
package lafsession
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupPair runs a full handshake and returns established client and server sessions.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkHandshake measures the full mutual-auth handshake state machine
|
||||||
|
// (message build/parse, transcript, AcceptPeer, key install). Crypto ops are
|
||||||
|
// inherent to the flow but not isolated.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSeal measures session record framing + seal path for a fixed LAF-sized payload.
|
||||||
|
func BenchmarkSeal(b *testing.B) {
|
||||||
|
cli, _ := setupPair(b)
|
||||||
|
payload := make([]byte, 256) // typical small LAF envelope
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkOpen measures open + anti-replay accept for sequential records.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSealOpenRoundTrip is client Seal → server Open for one payload size.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSealOpenSizes exercises framing logic across payload sizes.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkReplayWindow measures only the anti-replay window accept path.
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkReplayWindowWithGaps simulates out-of-order accepts within the window.
|
||||||
|
func BenchmarkReplayWindowWithGaps(b *testing.B) {
|
||||||
|
w := newReplayWindow(1024)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
seq := uint64(i)
|
||||||
|
// deliver even first, then the preceding odd on next iteration pattern
|
||||||
|
if i%2 == 0 {
|
||||||
|
if !w.accept(seq + 1) {
|
||||||
|
// may already be past; ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = w.accept(seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkFingerprint is package helper logic (hash + hex), not a crypto lib microbench.
|
||||||
|
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:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSealToOpenTo measures steady-state 0-alloc path with caller-owned buffers.
|
||||||
|
func BenchmarkSealToOpenTo(b *testing.B) {
|
||||||
|
cli, srv := setupPair(b)
|
||||||
|
payload := make([]byte, 256)
|
||||||
|
for i := range payload {
|
||||||
|
payload[i] = byte(i)
|
||||||
|
}
|
||||||
|
// capacity: header + pt + tag
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
// must copy frame before OpenTo if we reuse sealDst; use independent buffer
|
||||||
|
pt, err := srv.OpenTo(openDst[:0], frame)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(pt) != len(payload) {
|
||||||
|
b.Fatal("len")
|
||||||
|
}
|
||||||
|
// frame still points into sealDst; next SealTo with [:0] reuses same cap
|
||||||
|
_ = frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSealSteady uses Seal after warm-up; internal scratch avoids allocs.
|
||||||
|
func BenchmarkSealSteady(b *testing.B) {
|
||||||
|
cli, _ := setupPair(b)
|
||||||
|
payload := make([]byte, 256)
|
||||||
|
// warm-up grow
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkOpenSteady uses Open after warm-up with pre-copied frames.
|
||||||
|
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...)
|
||||||
|
}
|
||||||
|
// warm openBuf
|
||||||
|
if _, err := srv.Open(frames[0]); err != nil && err != ErrReplay {
|
||||||
|
// first frame opens; remaining still valid if seq advances
|
||||||
|
}
|
||||||
|
// re-setup pair for clean window
|
||||||
|
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...)
|
||||||
|
}
|
||||||
|
// warm
|
||||||
|
if _, err := srv.Open(append([]byte(nil), frames[0]...)); err == nil {
|
||||||
|
// consumed seq 0; rebuild
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
145
session_test.go
Normal file
145
session_test.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client → Server: ClientHello
|
||||||
|
ch, err := cli.StartHandshake()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server → Client: ServerHello
|
||||||
|
sh, err := srv.HandleHandshake(ch)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if sh == nil {
|
||||||
|
t.Fatal("expected ServerHello")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client → Server: ClientFinished
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server → Client: ServerFinished
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application data: LAF payload (opaque)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replay must fail
|
||||||
|
_, 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 // reject all
|
||||||
|
},
|
||||||
|
})
|
||||||
|
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