feat(server): enhance Home certificate handling with CA fingerprint verification
- Added support for `ClusterID`, `CAFingerprint`, and `EnrollmentSecret` in Home JWT claims. - Implemented CA fingerprint normalization and verification for PEM and file-based certificates. - Improved certificate request validation and error handling. - Updated server-side logic to include `EnrollmentSecret` in certificate requests.
This commit is contained in:
@@ -6,9 +6,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -26,10 +28,13 @@ import (
|
|||||||
const homeCertificateRequestTimeout = 30 * time.Second
|
const homeCertificateRequestTimeout = 30 * time.Second
|
||||||
|
|
||||||
type homeJWTClaims struct {
|
type homeJWTClaims struct {
|
||||||
CertificateID string `json:"certificate_id"`
|
CertificateID string `json:"certificate_id"`
|
||||||
IP string `json:"ip"`
|
ClusterID string `json:"cluster_id"`
|
||||||
Port int `json:"port"`
|
CAFingerprint string `json:"ca_fingerprint"`
|
||||||
IssuedAt int64 `json:"iat"`
|
EnrollmentSecret string `json:"enrollment_secret"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
IssuedAt int64 `json:"iat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type certificateRequestResponse struct {
|
type certificateRequestResponse struct {
|
||||||
@@ -88,6 +93,15 @@ func parseHomeJWTClaims(rawJWT string) (homeJWTClaims, error) {
|
|||||||
if strings.TrimSpace(claims.CertificateID) == "" {
|
if strings.TrimSpace(claims.CertificateID) == "" {
|
||||||
return claims, fmt.Errorf("home jwt certificate_id is required")
|
return claims, fmt.Errorf("home jwt certificate_id is required")
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(claims.ClusterID) == "" {
|
||||||
|
return claims, fmt.Errorf("home jwt cluster_id is required")
|
||||||
|
}
|
||||||
|
if normalizeFingerprint(claims.CAFingerprint) == "" {
|
||||||
|
return claims, fmt.Errorf("home jwt ca_fingerprint is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(claims.EnrollmentSecret) == "" {
|
||||||
|
return claims, fmt.Errorf("home jwt enrollment_secret is required")
|
||||||
|
}
|
||||||
if strings.TrimSpace(claims.IP) == "" || claims.Port <= 0 {
|
if strings.TrimSpace(claims.IP) == "" || claims.Port <= 0 {
|
||||||
return claims, fmt.Errorf("home jwt target address is invalid")
|
return claims, fmt.Errorf("home jwt target address is invalid")
|
||||||
}
|
}
|
||||||
@@ -120,6 +134,9 @@ func ensureHomeCertificateFiles(ctx context.Context, claims homeJWTClaims, paths
|
|||||||
if !fileExists(paths.CACert) {
|
if !fileExists(paths.CACert) {
|
||||||
return fmt.Errorf("home ca certificate file is missing")
|
return fmt.Errorf("home ca certificate file is missing")
|
||||||
}
|
}
|
||||||
|
if errVerify := verifyCACertificateFile(paths.CACert, claims.CAFingerprint); errVerify != nil {
|
||||||
|
return errVerify
|
||||||
|
}
|
||||||
if errChmod := chmodCertificateFiles(paths); errChmod != nil {
|
if errChmod := chmodCertificateFiles(paths); errChmod != nil {
|
||||||
return errChmod
|
return errChmod
|
||||||
}
|
}
|
||||||
@@ -143,6 +160,9 @@ func ensureHomeCertificateFiles(ctx context.Context, claims homeJWTClaims, paths
|
|||||||
if strings.TrimSpace(response.Certificate) == "" || strings.TrimSpace(response.CA) == "" {
|
if strings.TrimSpace(response.Certificate) == "" || strings.TrimSpace(response.CA) == "" {
|
||||||
return fmt.Errorf("home certificate response is incomplete")
|
return fmt.Errorf("home certificate response is incomplete")
|
||||||
}
|
}
|
||||||
|
if errVerify := verifyCACertificatePEM([]byte(response.CA), claims.CAFingerprint); errVerify != nil {
|
||||||
|
return errVerify
|
||||||
|
}
|
||||||
if errWrite := writeFile0600(paths.ClientCert, []byte(response.Certificate)); errWrite != nil {
|
if errWrite := writeFile0600(paths.ClientCert, []byte(response.Certificate)); errWrite != nil {
|
||||||
return errWrite
|
return errWrite
|
||||||
}
|
}
|
||||||
@@ -152,6 +172,49 @@ func ensureHomeCertificateFiles(ctx context.Context, claims homeJWTClaims, paths
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func verifyCACertificateFile(path string, expectedFingerprint string) error {
|
||||||
|
raw, errRead := os.ReadFile(path)
|
||||||
|
if errRead != nil {
|
||||||
|
return errRead
|
||||||
|
}
|
||||||
|
return verifyCACertificatePEM(raw, expectedFingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyCACertificatePEM(raw []byte, expectedFingerprint string) error {
|
||||||
|
actual, errFingerprint := certificateFingerprintPEM(raw)
|
||||||
|
if errFingerprint != nil {
|
||||||
|
return errFingerprint
|
||||||
|
}
|
||||||
|
expected := normalizeFingerprint(expectedFingerprint)
|
||||||
|
if expected == "" {
|
||||||
|
return fmt.Errorf("home ca fingerprint is required")
|
||||||
|
}
|
||||||
|
if actual != expected {
|
||||||
|
return fmt.Errorf("home ca fingerprint mismatch")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func certificateFingerprintPEM(raw []byte) (string, error) {
|
||||||
|
block, _ := pem.Decode(raw)
|
||||||
|
if block == nil || block.Type != "CERTIFICATE" {
|
||||||
|
return "", fmt.Errorf("home ca certificate pem is invalid")
|
||||||
|
}
|
||||||
|
cert, errParse := x509.ParseCertificate(block.Bytes)
|
||||||
|
if errParse != nil {
|
||||||
|
return "", errParse
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256(cert.Raw)
|
||||||
|
return hex.EncodeToString(sum[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeFingerprint(fingerprint string) string {
|
||||||
|
fingerprint = strings.TrimSpace(strings.ToLower(fingerprint))
|
||||||
|
fingerprint = strings.ReplaceAll(fingerprint, ":", "")
|
||||||
|
fingerprint = strings.ReplaceAll(fingerprint, " ", "")
|
||||||
|
return fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
func loadOrCreateClientKey(path string) (*rsa.PrivateKey, error) {
|
func loadOrCreateClientKey(path string) (*rsa.PrivateKey, error) {
|
||||||
if fileExists(path) {
|
if fileExists(path) {
|
||||||
raw, errRead := os.ReadFile(path)
|
raw, errRead := os.ReadFile(path)
|
||||||
@@ -252,7 +315,7 @@ func requestClientCertificate(ctx context.Context, claims homeJWTClaims, csrPEM
|
|||||||
if deadline, ok := dialCtx.Deadline(); ok {
|
if deadline, ok := dialCtx.Deadline(); ok {
|
||||||
_ = conn.SetDeadline(deadline)
|
_ = conn.SetDeadline(deadline)
|
||||||
}
|
}
|
||||||
if _, errWrite := conn.Write(encodeRESPArray("CERTIFICATE", "REQUEST", claims.CertificateID, string(csrPEM))); errWrite != nil {
|
if _, errWrite := conn.Write(encodeRESPArray("CERTIFICATE", "REQUEST", claims.CertificateID, claims.EnrollmentSecret, string(csrPEM))); errWrite != nil {
|
||||||
return response, errWrite
|
return response, errWrite
|
||||||
}
|
}
|
||||||
raw, errRead := readRESPBulk(bufio.NewReader(conn))
|
raw, errRead := readRESPBulk(bufio.NewReader(conn))
|
||||||
|
|||||||
Reference in New Issue
Block a user