Add cert-based re-authentication (OpAuthCert)
New flow: connect -> OpGetChallenge -> OpAuthCert (signature + certDER) -> server verifies cert chain + Ed25519 signature over challenge data -> reuses existing identity. No re-registration on reconnect.
This commit is contained in:
parent
0ed9d18b27
commit
73f09c29ab
5 changed files with 110 additions and 0 deletions
|
|
@ -148,3 +148,16 @@ func (ca *CA) CACert() *x509.Certificate {
|
|||
func (ca *CA) CACertPEM() []byte {
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.cert.Raw})
|
||||
}
|
||||
|
||||
func (ca *CA) VerifyCert(cert *x509.Certificate) error {
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(ca.cert)
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
if _, err := cert.Verify(opts); err != nil {
|
||||
return fmt.Errorf("cert verify: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,25 @@ func (m *Manager) Exists(number string) bool {
|
|||
return m.store.Has(numberKeyHash(number))
|
||||
}
|
||||
|
||||
func (m *Manager) GetByPubKey(pubKey [32]byte) (*Identity, error) {
|
||||
var found *Identity
|
||||
err := m.store.Iterate(func(keyHash uint64, payload []byte) bool {
|
||||
ident := m.unmarshalIdentity(payload)
|
||||
if ident != nil && ident.PubKey == pubKey {
|
||||
found = ident
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, m.masterKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("identity not found for pubkey")
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func numberKeyHash(number string) uint64 {
|
||||
h, _ := blake2b.New(8, nil)
|
||||
h.Write([]byte(number))
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import (
|
|||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/caddyserver/certmagic"
|
||||
|
|
@ -213,6 +215,8 @@ func (s *Server) handleStream(ctx context.Context, stream *quic.Stream, sess *Se
|
|||
s.handleSolve(sess, frame.Payload)
|
||||
case types.OpReroll:
|
||||
s.handleReroll(sess)
|
||||
case types.OpAuthCert:
|
||||
s.handleAuthCert(sess, frame.Payload)
|
||||
case types.OpDial:
|
||||
if !sess.IsAuthenticated() {
|
||||
sendOpError(st, types.ErrInvalidRequest, "not authenticated")
|
||||
|
|
@ -286,6 +290,78 @@ func (s *Server) handleSolve(sess *Session, payload []byte) {
|
|||
zap.String("number", ident.Number))
|
||||
}
|
||||
|
||||
func (s *Server) handleAuthCert(sess *Session, payload []byte) {
|
||||
challenge := sess.GetChallenge()
|
||||
if challenge == nil {
|
||||
sendOpError(sess.Stream(), types.ErrAuthFailed, "no challenge issued")
|
||||
return
|
||||
}
|
||||
if time.Since(time.Unix(0, challenge.Timestamp)) > s.cfg.PoW.ChallengeTTL {
|
||||
sendOpError(sess.Stream(), types.ErrAuthFailed, "challenge expired")
|
||||
return
|
||||
}
|
||||
|
||||
if len(payload) < 1+64+2 {
|
||||
sendOpError(sess.Stream(), types.ErrAuthFailed, "invalid auth payload")
|
||||
return
|
||||
}
|
||||
sigLen := int(payload[0])
|
||||
if sigLen != 64 || len(payload) < 1+sigLen+2 {
|
||||
sendOpError(sess.Stream(), types.ErrAuthFailed, "invalid signature length")
|
||||
return
|
||||
}
|
||||
signature := payload[1 : 1+sigLen]
|
||||
certLen := int(binary.BigEndian.Uint16(payload[1+sigLen:]))
|
||||
if len(payload) < 1+sigLen+2+certLen {
|
||||
sendOpError(sess.Stream(), types.ErrAuthFailed, "invalid cert length")
|
||||
return
|
||||
}
|
||||
certDER := payload[1+sigLen+2 : 1+sigLen+2+certLen]
|
||||
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
sendOpError(sess.Stream(), types.ErrAuthFailed, "invalid certificate")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.ca.VerifyCert(cert); err != nil {
|
||||
sendOpError(sess.Stream(), types.ErrAuthFailed, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
pubKeyRaw, ok := cert.PublicKey.(ed25519.PublicKey)
|
||||
if !ok || len(pubKeyRaw) != 32 {
|
||||
sendOpError(sess.Stream(), types.ErrAuthFailed, "invalid public key")
|
||||
return
|
||||
}
|
||||
|
||||
challengeData := challenge.Marshal()
|
||||
if !ed25519.Verify(pubKeyRaw, challengeData, signature) {
|
||||
sendOpError(sess.Stream(), types.ErrAuthFailed, "signature verification failed")
|
||||
return
|
||||
}
|
||||
|
||||
var pubKey [32]byte
|
||||
copy(pubKey[:], pubKeyRaw)
|
||||
|
||||
ident, err := s.identMgr.GetByPubKey(pubKey)
|
||||
if err != nil {
|
||||
sendOpError(sess.Stream(), types.ErrAuthFailed, "identity not found")
|
||||
return
|
||||
}
|
||||
|
||||
sess.Authenticate(ident.Number, pubKey, certDER)
|
||||
s.Register(sess, ident.Number)
|
||||
|
||||
respPayload := marshalIdentityResponse(ident.Number, certDER, s.ca.CACertPEM())
|
||||
if st := sess.Stream(); st != nil {
|
||||
st.WriteFrame(byte(types.OpIdentity), respPayload)
|
||||
}
|
||||
|
||||
s.logger.Info("Client re-authenticated",
|
||||
zap.String("number", ident.Number))
|
||||
}
|
||||
|
||||
func (s *Server) handleReroll(sess *Session) {
|
||||
if !sess.IsAuthenticated() {
|
||||
sendOpError(sess.Stream(), types.ErrInvalidRequest, "not authenticated")
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const (
|
|||
OpSolve OpCode = 0x03
|
||||
OpIdentity OpCode = 0x04
|
||||
OpReroll OpCode = 0x05
|
||||
OpAuthCert OpCode = 0x06
|
||||
|
||||
OpDial OpCode = 0x10
|
||||
OpRing OpCode = 0x11
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const (
|
|||
ErrBusy ErrorCode = 0x0A
|
||||
ErrSelfCall ErrorCode = 0x0B
|
||||
ErrRateLimited ErrorCode = 0x0C
|
||||
ErrAuthFailed ErrorCode = 0x0D
|
||||
)
|
||||
|
||||
type CodecType uint8
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue