API version of: ziti edge enroll --jwt identity.jwt --out identity.json

The docs show the below command to create a json file from an identity jwt. What would be the functional equivalen for this enroll command that returns this json using the zitii api?

I've tried the endpoint /edge/client/v1/enroll?method=ott&id={token} where token is the jwt, but getting an INVALID_ENROLLMENT_TOKEN error response. I have also tried using the id of the actual identity as the value for token.

  • command
./ziti-edge-tunnel enroll --jwt ./myTunneler.jwt --identity ./myTunneler.json
  • output
{
    "ztAPI": "https://ziti.com:1280/edge/client/v1",
    "ztAPIs": null,
    "configTypes": null,
    "id": {
        "key": "pem:-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----\n",
        "cert": "pem:-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----\n",
        "ca": "pem:-----BEGIN CERTIFICATE-----...-----\n"
    },
    "enableHa": false
}

Which language are you working in? I'll link you directly to an SDK sample if we've got one. If not then I'll help you navigate the spec to get this done.

We are using ruby to make requests to the api.
If its simpler to do with python sdk, then that could be an option..

1 Like

I'm sure it's doable in Ruby, but I'm glad you mentioned Python. Here's how the Py SDK does it, by importing Ziti's C library.

def enroll(jwt, key=None, cert=None):
    """
    Enroll Ziti Identity
    :param jwt: (required) enrollment token,
        can be either name of the token file or a string containing JWT
    :param key: private key to use for enrollment
        (required for 3rd party CA enrollment, otherwise optional,
        new key is generated if None)
    :param cert: certificate to use for enrollment
        (required for 3rd party CA enrollment)
    :return: string containing Ziti Identity in JSON format
    """
    init()
    try:
        with open(jwt, 'rb') as jwt_f:
            jwtc = bytes(jwt_f.read())
    except:
        jwtc = bytes(jwt, 'utf-8')

    id_json = ctypes.c_char_p()
    id_json_len = ctypes.c_size_t()

    keyb = None if key is None else bytes(key, 'utf-8')
    certb = None if cert is None else bytes(cert, 'utf-8')

    retcode = _ziti_enroll(jwtc, keyb, certb,
                           ctypes.byref(id_json),
                           ctypes.byref(id_json_len))
    if retcode != 0:
        raise RuntimeError(errorstr(retcode))
    try:
        return id_json.value.decode()
    finally:
        _free(id_json)

from: ziti-sdk-py/src/openziti/zitilib.py at main · openziti/ziti-sdk-py · GitHub

This function can be called by invoking the module's enroll command.

python -m openziti enroll --jwt /tmp/py1.jwt --identity /tmp/py1.json

It would be good to document the REST requests for this so I'll also work on doing that for Python.

1 Like

Thanks, I'll try running this an reply asap

It worked. I'm using it within the code rather than through cli.

import openziti

def get_jwt():
   "retuns either jwt raw string or filepath to jwt file"
   ...

jwt = get_jwt()

enrolled_json = openziti.enroll(jwt)
print(enrolled_json)

Awesome. Just remembered these exist and realized they are probably now stale since some time has passed since the spec they were generated from.

Still, these might be useful.

Client library would be used by the same app that is enrolling: GitHub - openziti-test-kitchen/openziti-edge-client-python: Python Library Implementing the OpenZiti Edge Client API

Mgmt library would be used for managing Ziti entities, including client identities: GitHub - openziti-test-kitchen/openziti-edge-management-python: Python library for the Ziti Management API

Here's a post by the developer explaining how and why these libraries were generated: OpenAPI Python Clients

Enroll() from the Go SDK will be the best reference. Let's translate this to Python or Ruby or both.

func Enroll(enFlags EnrollmentFlags) (*ziti.Config, error) {
	var key crypto.PrivateKey
	var err error

	cfg := &ziti.Config{
		ZtAPI: edge_apis.ClientUrl(enFlags.Token.Issuer),
	}

	if strings.TrimSpace(enFlags.KeyFile) != "" {
		stat, err := os.Stat(enFlags.KeyFile)

		if stat != nil && !os.IsNotExist(err) {
			if stat.IsDir() {
				return nil, errors.Errorf("specified key is a directory (%s)", enFlags.KeyFile)
			}

			if absPath, fileErr := filepath.Abs(enFlags.KeyFile); fileErr != nil {
				return nil, fileErr
			} else {
				cfg.ID.Key = "file://" + absPath
			}

		} else {
			cfg.ID.Key = enFlags.KeyFile
			pfxlog.Logger().Infof("using engine : %s\n", strings.Split(enFlags.KeyFile, ":")[0])
		}
	} else {
		var asnBytes []byte
		var keyPem []byte
		if enFlags.KeyAlg.EC() {
			key, err = generateECKey()
			asnBytes, _ := x509.MarshalECPrivateKey(key.(*ecdsa.PrivateKey))
			keyPem = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: asnBytes})
		} else if enFlags.KeyAlg.RSA() {
			key, err = generateRSAKey()
			asnBytes = x509.MarshalPKCS1PrivateKey(key.(*rsa.PrivateKey))
			keyPem = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: asnBytes})
		} else {
			panic(fmt.Sprintf("invalid KeyAlg specified: %s", enFlags.KeyAlg.Get()))
		}
		cfg.ID.Key = "pem:" + string(keyPem)
		if err != nil {
			return nil, err
		}
	}

	if enFlags.CertFile != "" {
		enFlags.CertFile, _ = filepath.Abs(enFlags.CertFile)
		cfg.ID.Cert = "file://" + enFlags.CertFile
	}

	caPool, allowedCerts := enFlags.GetCertPool()

	//fetch so CA bundles
	pfxlog.Logger().Debug("fetching certificates from server")
	serverOnlyCaPool := x509.NewCertPool()
	serverOnlyCaPool.AddCert(enFlags.Token.SignatureCert)

	controllerCas := FetchCertificates(cfg.ZtAPI, serverOnlyCaPool)

	if len(controllerCas) == 0 {
		return nil, errors.New("expected 1 or more CAs from controller, got 0")
	}

	for _, cert := range controllerCas {
		allowedCerts = append(allowedCerts, cert)
		caPool.AddCert(cert)
	}

	var enrollErr error
	switch enFlags.Token.EnrollmentMethod {
	case "ott":
		enrollErr = enrollOTT(enFlags.Token, cfg, caPool)
	case "ottca":
		enrollErr = enrollCA(enFlags.Token, cfg, caPool)
	case "ca":
		enrollErr = enrollCAAuto(enFlags, cfg, caPool)
	default:
		enrollErr = errors.Errorf("enrollment method '%s' is not supported", enFlags.Token.EnrollmentMethod)
	}

	if enrollErr != nil {
		return nil, enrollErr
	}

	if len(allowedCerts) > 0 {
		var buf bytes.Buffer

		err := nfx509.MarshalToPem(allowedCerts, &buf)

		if err != nil {
			return nil, err
		}

		cfg.ID.CA = "pem:" + buf.String()
	}

	cfg.Credentials = edge_apis.NewIdentityCredentialsFromConfig(cfg.ID)

	return cfg, nil
}

This code snippet defines a function called Enroll in Go. The function takes an EnrollmentFlags parameter and returns a Ziti context, which is a data structure containing an identity typically stored as JSON.

The function starts by checking whether a private key is provided and generates one if not. The same function can be used to enroll an identity with a certificate from a trusted authority or to request a certificate from the edge enrollment signer CA managed by the Ziti controller, so there's an affordance here for an cert to be provided instead of issued.

The function then fetches the well-known (trusted) certificates from the client API provided by the controller.

Based on the enFlags.Token.EnrollmentMethod value, the function calls different enrollment methods (enrollOTT, enrollCA, enrollCAAuto) and assigns the result to the enrollErr variable.

We'll follow the enrollOTT possibility here because it's the most relevant. This ott method is called when enrolling with a one time token (JWT) and requesting a certificate to be issued.

Here's a summary of what enrollOTT does:

  1. It loads the private key.
  2. It creates a certificate request as PEM.
  3. It sends a POST request to the enrollment URL specified in token.EnrolmentUrl() with the certificate request in PEM format.
  4. If the status code is http.StatusOK, it checks the content-type header. If it's application/json, it parses the response body as JSON and extracts the data.cert field, which is assumed to contain the certificate in PEM format. It then sets cfg.ID.Cert to this value.
  5. If the content-type header is not application/json, it treats the response body as PEM and sets cfg.ID.Cert to this value.

Finally, the function returns a Ziti context object from the key, cert, and well-known pool.

1 Like