diff --git a/cmd/zerotier/cli/cert.go b/cmd/zerotier/cli/cert.go index d9ce87efa..d7a2f0cef 100644 --- a/cmd/zerotier/cli/cert.go +++ b/cmd/zerotier/cli/cert.go @@ -28,8 +28,6 @@ func Cert(basePath string, authTokenGenerator func() string, args []string, json switch args[0] { - case "list": - case "newsid": if len(args) > 2 { Help() @@ -86,17 +84,18 @@ func Cert(basePath string, authTokenGenerator func() string, args []string, json Help() return 1 } - var csr zerotier.Certificate + csrBytes, err := ioutil.ReadFile(args[1]) if err != nil { fmt.Printf("ERROR: unable to read CSR from %s: %s\n", args[1], err.Error()) return 1 } - c, err := zerotier.NewCertificateFromBytes(csrBytes, false) + csr, err := zerotier.NewCertificateFromBytes(csrBytes, false) if err != nil { fmt.Printf("ERROR: CSR in %s is invalid: %s\n", args[1], err.Error()) return 1 } + id := readIdentity(args[2]) if id == nil { fmt.Printf("ERROR: unable to read identity from %s\n", args[2]) @@ -106,12 +105,13 @@ func Cert(basePath string, authTokenGenerator func() string, args []string, json fmt.Printf("ERROR: signing identity in %s lacks private key\n", args[2]) return 1 } - c, err = csr.Sign(id) + + cert, err := csr.Sign(id) if err != nil { fmt.Printf("ERROR: error signing CSR or generating certificate: %s\n", err.Error()) return 1 } - cb, err := c.Marshal() + cb, err := cert.Marshal() if err != nil { fmt.Printf("ERROR: error marshaling signed certificate: %s\n", err.Error()) return 1 @@ -124,7 +124,28 @@ func Cert(basePath string, authTokenGenerator func() string, args []string, json return 1 } - case "verify": + case "verify", "dump": + if len(args) != 2 { + Help() + return 1 + } + certBytes, err := ioutil.ReadFile(args[1]) + if err != nil { + fmt.Printf("ERROR: unable to read certificate from %s: %s\n", args[1], err.Error()) + return 1 + } + cert, err := zerotier.NewCertificateFromBytes(certBytes, true) + if err != nil { + fmt.Printf("FAILED: certificate in %s invalid: %s\n", args[1], err.Error()) + return 1 + } + if args[0] == "dump" { + fmt.Println(cert.JSON()) + } else { + fmt.Println("OK") + } + + case "list": case "show": if len(args) != 1 { diff --git a/cmd/zerotier/cli/help.go b/cmd/zerotier/cli/help.go index fae4a13f9..85e97b314 100644 --- a/cmd/zerotier/cli/help.go +++ b/cmd/zerotier/cli/help.go @@ -37,7 +37,7 @@ Common Operations: help Show this help version Print version - status Show node status and configuration +· status Show node status and configuration · set [option] [value] - Get or set node configuration port Primary P2P port @@ -95,11 +95,12 @@ Advanced Operations: cert [args] - Certificate management · list List certificates in local node store -· show [serial] List or show details of a certificate +· show List or show details of a certificate newsid Create a new subject unique ID newcsr Create a subject CSR sign Sign a CSR to create a certificate -· verify Verify a certificate + verify Verify certificate (not entire chain) + dump Verify and print certificate · import [trust,[trust]] Import certificate into this node rootca Certificate is a root CA (trust flag) ztrootset ZeroTier root node set (trust flag) diff --git a/core/zerotier.h b/core/zerotier.h index 54189af7e..3b64d7ba6 100644 --- a/core/zerotier.h +++ b/core/zerotier.h @@ -2411,7 +2411,7 @@ ZT_SDK_API int ZT_Node_tryPeer( * @param cert Certificate object, or set to NULL if certData and certSize are to be used * @param certData Certificate binary data if 'cert' is NULL, NULL otherwise * @param certSize Size of certificate binary data, 0 if none - * @return + * @return Certificate error or ZT_CERTIFICATE_ERROR_NONE on success */ ZT_SDK_API enum ZT_CertificateError ZT_Node_addCertificate( ZT_Node *node, @@ -2422,6 +2422,22 @@ ZT_SDK_API enum ZT_CertificateError ZT_Node_addCertificate( const void *certData, unsigned int certSize); +/** + * Delete a certificate from this node's certificate store + * + * Note that deleting CA certificates may also imply deletion of certificates + * that depend on them for full chain verification. + * + * @param node Node instance + * @param tptr Thread pointer to pass to functions/callbacks resulting from this call + * @param serialNo 48-byte / 384-bit serial number of certificate to delete + * @return OK (0) or error code + */ +ZT_SDK_API enum ZT_ResultCode ZT_Node_deleteCertificate( + ZT_Node *node, + void *tptr, + const void *serialNo); + /** * List certificates installed in this node's trust store * diff --git a/go.mod b/go.mod index 01ad7e23a..be25b71ab 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,5 @@ go 1.13 require ( github.com/Microsoft/go-winio v0.4.14 github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 - golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect + golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d // indirect ) diff --git a/go.sum b/go.sum index d1243faf5..1c1ccab83 100644 --- a/go.sum +++ b/go.sum @@ -13,5 +13,5 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190529164535-6a60838ec259 h1:so6Hr/LodwSZ5UQDu/7PmQiDeS112WwtLvU3lpSPZTU= golang.org/x/sys v0.0.0-20190529164535-6a60838ec259/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d h1:QQrM/CCYEzTs91GZylDCQjGHudbPTxF/1fvXdVh5lMo= +golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/zerotier/api.go b/pkg/zerotier/api.go index e7ab54ef5..c87ff1a58 100644 --- a/pkg/zerotier/api.go +++ b/pkg/zerotier/api.go @@ -16,6 +16,7 @@ package zerotier import ( "bytes" secrand "crypto/rand" + "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -217,10 +218,12 @@ func apiCheckAuth(out http.ResponseWriter, req *http.Request, token string) bool if len(ah) > 0 && strings.TrimSpace(ah) == ("bearer "+token) { return true } + ah = req.Header.Get("X-ZT1-Auth") if len(ah) > 0 && strings.TrimSpace(ah) == token { return true } + _ = apiSendObj(out, req, http.StatusUnauthorized, &APIErr{"authorization token not found or incorrect (checked X-ZT1-Auth and Authorization headers)"}) return false } @@ -252,6 +255,8 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e smux := http.NewServeMux() + // ----------------------------------------------------------------------------------------------------------------- + smux.HandleFunc("/status", func(out http.ResponseWriter, req *http.Request) { defer func() { e := recover() @@ -298,6 +303,8 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e } }) + // ----------------------------------------------------------------------------------------------------------------- + smux.HandleFunc("/config", func(out http.ResponseWriter, req *http.Request) { defer func() { e := recover() @@ -330,6 +337,8 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e } }) + // ----------------------------------------------------------------------------------------------------------------- + smux.HandleFunc("/peer/", func(out http.ResponseWriter, req *http.Request) { var err error @@ -397,6 +406,8 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e } }) + // ----------------------------------------------------------------------------------------------------------------- + smux.HandleFunc("/network/", func(out http.ResponseWriter, req *http.Request) { defer func() { e := recover() @@ -421,6 +432,7 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e } if req.Method == http.MethodDelete { + if queriedID == 0 { _ = apiSendObj(out, req, http.StatusBadRequest, &APIErr{"only specific networks can be deleted"}) return @@ -434,7 +446,9 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e } } _ = apiSendObj(out, req, http.StatusNotFound, &APIErr{"network not found"}) + } else if req.Method == http.MethodPost || req.Method == http.MethodPut { + if queriedID == 0 { _ = apiSendObj(out, req, http.StatusBadRequest, nil) return @@ -460,7 +474,9 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e _ = apiSendObj(out, req, http.StatusOK, apiNetworkFromNetwork(n)) } } + } else if req.Method == http.MethodGet || req.Method == http.MethodHead { + networks := node.Networks() if queriedID == 0 { // no queried ID lists all networks nws := make([]*APINetwork, 0, len(networks)) @@ -477,12 +493,82 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e } } _ = apiSendObj(out, req, http.StatusNotFound, &APIErr{"network not found"}) + } else { out.Header().Set("Allow", "GET, HEAD, PUT, POST, DELETE") _ = apiSendObj(out, req, http.StatusMethodNotAllowed, &APIErr{"unsupported method " + req.Method}) } }) + // ----------------------------------------------------------------------------------------------------------------- + + smux.HandleFunc("/cert/", func(out http.ResponseWriter, req *http.Request) { + defer func() { + e := recover() + if e != nil { + _ = apiSendObj(out, req, http.StatusInternalServerError, nil) + } + }() + + if !apiCheckAuth(out, req, authToken) { + return + } + apiSetStandardHeaders(out) + + var queriedSerialNo []byte + if len(req.URL.Path) > 6 { + b, err := base64.URLEncoding.DecodeString(req.URL.Path[6:]) + if err != nil || len(b) != CertificateSerialNoSize { + _ = apiSendObj(out, req, http.StatusBadRequest, &APIErr{"invalid base64 serial number in certificate path"}) + return + } + queriedSerialNo = b + } + + if req.Method == http.MethodGet || req.Method == http.MethodHead { + + certs, err := node.ListCertificates() + if err != nil { + _ = apiSendObj(out, req, http.StatusInternalServerError, &APIErr{"unexpected error listing certificates"}) + return + } + + if len(queriedSerialNo) == CertificateSerialNoSize { + for _, c := range certs { + if bytes.Equal(c.Certificate.SerialNo, queriedSerialNo) { + _ = apiSendObj(out, req, http.StatusOK, c) + break + } + } + } else { + _ = apiSendObj(out, req, http.StatusOK, certs) + } + + } else if req.Method == http.MethodPost || req.Method == http.MethodPut { + + var lc LocalCertificate + if apiReadObj(out, req, &lc) == nil { + if lc.Certificate == nil { + _ = apiSendObj(out, req, http.StatusBadRequest, &APIErr{"missing certificate"}) + return + } + } + + } else if req.Method == http.MethodDelete { + + if len(queriedSerialNo) == CertificateSerialNoSize { + } else { + _ = apiSendObj(out, req, http.StatusNotFound, &APIErr{"certificate not found"}) + } + + } else { + out.Header().Set("Allow", "GET, HEAD, PUT, POST, DELETE") + _ = apiSendObj(out, req, http.StatusMethodNotAllowed, &APIErr{"unsupported method " + req.Method}) + } + }) + + // ----------------------------------------------------------------------------------------------------------------- + listener, err := createNamedSocketListener(basePath, APISocketName) if err != nil { return nil, nil, err diff --git a/pkg/zerotier/certificate.go b/pkg/zerotier/certificate.go index 79dd9a571..b1f0b18ed 100644 --- a/pkg/zerotier/certificate.go +++ b/pkg/zerotier/certificate.go @@ -18,6 +18,7 @@ package zerotier import "C" import ( + "encoding/base64" "encoding/json" "fmt" "unsafe" @@ -96,6 +97,12 @@ type CertificateSubjectUniqueIDSecret struct { UniqueIDSecret []byte `json:"uniqueIdSecret,omitempty"` } +// LocalCertificate combines a certificate with its local trust flags. +type LocalCertificate struct { + Certificate *Certificate `json:"certificate,omitempty"` + LocalTrust uint `json:"localTrust"` +} + func certificateErrorToError(cerr int) error { switch cerr { case C.ZT_CERTIFICATE_ERROR_NONE: @@ -488,6 +495,11 @@ func (c *Certificate) JSON() string { return string(j) } +// URLSerialNo returns the serial number encoded for use in /cert/### local API URLs. +func (c *Certificate) URLSerialNo() string { + return base64.URLEncoding.EncodeToString(c.SerialNo) +} + // NewCertificateSubjectUniqueId creates a new certificate subject unique ID and corresponding private key. // Right now only one type is supported: CertificateUniqueIdTypeNistP384 func NewCertificateSubjectUniqueId(uniqueIdType int) (id []byte, priv []byte, err error) { diff --git a/pkg/zerotier/node.go b/pkg/zerotier/node.go index a9b5d1911..1b4f71cff 100644 --- a/pkg/zerotier/node.go +++ b/pkg/zerotier/node.go @@ -519,7 +519,7 @@ func (n *Node) TryPeer(fpOrAddress interface{}, ep *Endpoint, retries int) bool } // ListCertificates lists certificates and their corresponding local trust flags. -func (n *Node) ListCertificates() (certs []*Certificate, localTrust []uint, err error) { +func (n *Node) ListCertificates() (certs []LocalCertificate, err error) { cl := C.ZT_Node_listCertificates(n.zn) if cl != nil { defer C.ZT_freeQueryResult(unsafe.Pointer(cl)) @@ -527,15 +527,22 @@ func (n *Node) ListCertificates() (certs []*Certificate, localTrust []uint, err c := newCertificateFromCCertificate(unsafe.Pointer(uintptr(unsafe.Pointer(cl.certs)) + (i * pointerSize))) if c != nil { lt := *((*C.uint)(unsafe.Pointer(uintptr(unsafe.Pointer(cl.localTrust)) + (i * C.sizeof_int)))) - certs = append(certs, c) - localTrust = append(localTrust, uint(lt)) + certs = append(certs, LocalCertificate{Certificate: c, LocalTrust: uint(lt)}) } } } return } -// -------------------------------------------------------------------------------------------------------------------- +// AddCertificate adds a certificate to this node's local certificate store (after verification). +func (n *Node) AddCertificate(cert *Certificate) error { +} + +// DeleteCertificate deletes a certificate from this node's local certificate store. +func (n *Node) DeleteCertificate(serialNo []byte) error { +} + +// ------------------------------------------------------------------------------------------------------------------- func (n *Node) runMaintenance() { n.localConfigLock.RLock() diff --git a/vendor/modules.txt b/vendor/modules.txt index d139c95d5..57442a3a8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,6 +4,6 @@ github.com/Microsoft/go-winio/pkg/guid # github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 github.com/hectane/go-acl github.com/hectane/go-acl/api -# golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae +# golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d golang.org/x/sys/internal/unsafeheader golang.org/x/sys/windows