mirror of
https://github.com/hanzozt/zt.git
synced 2026-10-30 15:04:55 +00:00
The defaults behind every certificate subject named upstream's organization, a development unit and an invented city and state, so the CAs, servers, clients and routers the bootstrap mints all carried them. They now read O=Hanzo AI, OU=ZT, C=US and leave ST and L unset. A router put an unset attribute into its CSR as an empty value, which is not a DirectoryString (RFC 5280 4.1.2.4); it now leaves it out. And `pki create` read the province from a key its flag was never bound to, so --pki-province did nothing. Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
277 lines
7.1 KiB
Go
277 lines
7.1 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package enroll
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/hanzozt/zt/v2/router/env"
|
|
|
|
"github.com/go-resty/resty/v2"
|
|
"github.com/hanzozt/edge-api/rest_model"
|
|
"github.com/hanzozt/identity/certtools"
|
|
"github.com/hanzozt/sdk-golang/zt"
|
|
"github.com/hanzozt/sdk-golang/zt/enroll"
|
|
"github.com/michaelquigley/pfxlog"
|
|
)
|
|
|
|
type apiPost struct {
|
|
ServerCertCsr string `json:"serverCertCsr"`
|
|
CertCsr string `json:"certCsr"`
|
|
}
|
|
|
|
type Enroller interface {
|
|
Enroll(jwt []byte, silent bool, engine string, keyAlg zt.KeyAlgVar) error
|
|
}
|
|
|
|
type RestEnroller struct {
|
|
fullConfig *env.Config
|
|
config *env.EdgeConfig
|
|
|
|
// Issuer, when set, replaces the token's issuer as the controller address
|
|
// enrollment verifies against and posts to. A router beside its controller
|
|
// enrolls over loopback while the token names the public address, which
|
|
// may sit behind a TLS-terminating proxy.
|
|
Issuer string
|
|
}
|
|
|
|
func NewRestEnroller(config *env.Config) *RestEnroller {
|
|
return &RestEnroller{
|
|
fullConfig: config,
|
|
config: config.Edge,
|
|
}
|
|
}
|
|
|
|
func (re *RestEnroller) Enroll(jwtBuf []byte, silent bool, engine string, keyAlg zt.KeyAlgVar) error {
|
|
log := pfxlog.Logger()
|
|
|
|
if re.config == nil {
|
|
return errors.New("no configuration provided")
|
|
}
|
|
|
|
identityConfig := re.fullConfig.IdConfig
|
|
|
|
if re.config.RouterConfig.Id != nil {
|
|
log.Warnf("identity detected, note that any identity information will be overwritten when enrolling")
|
|
}
|
|
|
|
ec, err := parseToken(strings.TrimSpace(string(jwtBuf)), re.Issuer)
|
|
if err != nil {
|
|
log.WithField("cause", err).Fatal("failed to parse JWT")
|
|
}
|
|
|
|
log.Debug("JWT parsed")
|
|
|
|
rootCaPool := x509.NewCertPool()
|
|
rootCaPool.AddCert(ec.SignatureCert)
|
|
|
|
rootCas := enroll.FetchCertificates(ec.Issuer, rootCaPool)
|
|
|
|
if len(rootCas) == 0 {
|
|
log.Fatal("no valid root CAs were found")
|
|
}
|
|
|
|
var engUrl *url.URL
|
|
|
|
if engine != "" {
|
|
if engUrl, err = url.Parse(engine); err != nil {
|
|
return fmt.Errorf("could not parse engine string: %s", err)
|
|
}
|
|
}
|
|
|
|
//writes key if it is file based
|
|
var key crypto.PrivateKey
|
|
if keyAlg.EC() {
|
|
key, err = certtools.GetKey(engUrl, identityConfig.Key, "ec:P-256")
|
|
} else if keyAlg.RSA() {
|
|
key, err = certtools.GetKey(engUrl, identityConfig.Key, "rsa:4096")
|
|
} else {
|
|
panic(fmt.Sprintf("invalid KeyAlg specified: %s", keyAlg.Get()))
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("could not obtain private key: %s", err)
|
|
}
|
|
|
|
subject := csrSubject(ec.Subject, re.config.Csr)
|
|
|
|
serverCsr, err := CreateCsr(key, x509.UnknownSignatureAlgorithm, subject, re.config.Csr.Sans)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate server CSR: %s", err)
|
|
}
|
|
|
|
clientCsr, err := CreateCsr(key, x509.UnknownSignatureAlgorithm, subject, re.config.Csr.Sans)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate client CSR: %s", err)
|
|
}
|
|
|
|
er := &apiPost{
|
|
CertCsr: clientCsr,
|
|
ServerCertCsr: serverCsr,
|
|
}
|
|
|
|
client := resty.New()
|
|
|
|
caCertPool := x509.NewCertPool()
|
|
for _, cert := range rootCas {
|
|
caCertPool.AddCert(cert)
|
|
}
|
|
|
|
tc := &tls.Config{
|
|
RootCAs: caCertPool,
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
|
|
client.SetTLSClientConfig(tc)
|
|
|
|
envelope, err := re.Send(client, ec.EnrolmentUrl(), er)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp := envelope.Data
|
|
|
|
if resp.Cert == "" {
|
|
return fmt.Errorf("enrollment response did not contain a cert")
|
|
}
|
|
|
|
if resp.ServerCert == "" {
|
|
return fmt.Errorf("enrollment response did not contain a server cert")
|
|
}
|
|
|
|
if resp.Ca == "" {
|
|
return fmt.Errorf("enrollment response did not contain a CA chain")
|
|
}
|
|
|
|
if err = os.WriteFile(identityConfig.Cert, []byte(resp.Cert), 0600); err != nil {
|
|
return fmt.Errorf("unable to write client cert to [%s]: %s", identityConfig.Cert, err)
|
|
}
|
|
|
|
if err = os.WriteFile(identityConfig.ServerCert, []byte(resp.ServerCert), 0600); err != nil {
|
|
return fmt.Errorf("unable to write server cert to [%s]: %s", identityConfig.ServerCert, err)
|
|
}
|
|
|
|
if err = os.WriteFile(identityConfig.CA, []byte(resp.Ca), 0600); err != nil {
|
|
return fmt.Errorf("unable to write CA certs to [%s]: %s", identityConfig.CA, err)
|
|
}
|
|
|
|
var controllers []string
|
|
if len(re.fullConfig.Ctrl.InitialEndpoints) > 0 {
|
|
for _, ep := range re.fullConfig.Ctrl.InitialEndpoints {
|
|
controllers = append(controllers, ep.String())
|
|
}
|
|
} else {
|
|
// if config is missing endpoints, try to get them from the JWT claimset
|
|
claims := jwt.MapClaims{}
|
|
parser := jwt.NewParser()
|
|
_, _, err := parser.ParseUnverified(strings.TrimSpace(string(jwtBuf)), claims)
|
|
if err == nil {
|
|
if ctrl, ok := claims["ctrl"]; ok {
|
|
controllers = append(controllers, ctrl.(string))
|
|
}
|
|
|
|
if ctrls, ok := claims["ctrls"]; ok {
|
|
if ctrlsSlice, ok := ctrls.([]interface{}); ok {
|
|
for _, ctrl := range ctrlsSlice {
|
|
controllers = append(controllers, ctrl.(string))
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log.Warnf("failed to parse JWT for custom claims: %v", err)
|
|
}
|
|
}
|
|
|
|
if len(controllers) == 0 {
|
|
controllers = ec.Controllers
|
|
}
|
|
|
|
if err = re.fullConfig.SaveControllerEndpoints(controllers); err != nil {
|
|
return err
|
|
}
|
|
|
|
if re.fullConfig.Edge.Db != "" {
|
|
var fileInfo os.FileInfo
|
|
if fileInfo, err = os.Stat(re.fullConfig.Edge.Db); err == nil {
|
|
if !fileInfo.IsDir() {
|
|
log.WithField("path", re.fullConfig.Edge.Db).Info("deleting existing router data model save file")
|
|
if err = os.Remove(re.fullConfig.Edge.Db); err != nil {
|
|
log.WithField("path", re.fullConfig.Edge.Db).WithError(err).
|
|
Error("failed to delete existing router data model save file")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Info("registration complete")
|
|
return nil
|
|
}
|
|
|
|
// parseToken verifies an enrollment token against the certificate its issuer
|
|
// serves, reading the issuer from the token unless one is given.
|
|
func parseToken(token, issuer string) (*zt.EnrollmentClaims, error) {
|
|
if issuer == "" {
|
|
ec, _, err := enroll.ParseToken(token)
|
|
return ec, err
|
|
}
|
|
|
|
ec := &zt.EnrollmentClaims{}
|
|
_, err := jwt.NewParser().ParseWithClaims(token, ec, func(t *jwt.Token) (any, error) {
|
|
ec.Issuer = issuer
|
|
return enroll.ValidateToken(t)
|
|
})
|
|
return ec, err
|
|
}
|
|
|
|
func (re *RestEnroller) Send(client *resty.Client, enrollUrl string, e *apiPost) (*rest_model.EnrollmentCertsEnvelope, error) {
|
|
envelope := rest_model.EnrollmentCertsEnvelope{}
|
|
|
|
resp, err := client.R().
|
|
SetBody(e).
|
|
Post(enrollUrl)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
body := resp.Body()
|
|
|
|
err = json.Unmarshal(body, &envelope)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode() != http.StatusOK {
|
|
return nil, fmt.Errorf("enrollment failed received HTTP status [%s]: %s", resp.Status(), resp.Body())
|
|
}
|
|
|
|
return &envelope, nil
|
|
}
|