diff --git a/internal/ca/ca.go b/internal/ca/ca.go index 638d050..b3c2151 100644 --- a/internal/ca/ca.go +++ b/internal/ca/ca.go @@ -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 +} diff --git a/internal/identity/manager.go b/internal/identity/manager.go index 304624e..7588aa1 100644 --- a/internal/identity/manager.go +++ b/internal/identity/manager.go @@ -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)) diff --git a/internal/server/server.go b/internal/server/server.go index e8aa272..31c7e92 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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") diff --git a/pkg/types/opcode.go b/pkg/types/opcode.go index 2cbf14a..144df9f 100644 --- a/pkg/types/opcode.go +++ b/pkg/types/opcode.go @@ -8,6 +8,7 @@ const ( OpSolve OpCode = 0x03 OpIdentity OpCode = 0x04 OpReroll OpCode = 0x05 + OpAuthCert OpCode = 0x06 OpDial OpCode = 0x10 OpRing OpCode = 0x11 diff --git a/pkg/types/types.go b/pkg/types/types.go index b8ddc8f..353528c 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -24,6 +24,7 @@ const ( ErrBusy ErrorCode = 0x0A ErrSelfCall ErrorCode = 0x0B ErrRateLimited ErrorCode = 0x0C + ErrAuthFailed ErrorCode = 0x0D ) type CodecType uint8