AnnouncementsIntroducing Arkoren

    Menu

    TLS Certificates

    Provision local identities, load certificate files, and configure server trust and mutual TLS without weakening verification.

    What's on this page
    1. Distinguish publicly trusted production identities from explicit local-development provisioning.
    2. Generate local server certificates and load existing identities without changing files or system trust.
    3. Choose private-only or additive system roots while preserving hostname and certificate verification.
    4. Configure both sides of mutual TLS and keep authentication separate from application authorization.
    5. Share detached certificate exports safely without mistaking native TLS clones for deep copies.
    On this page

    HTTPS needs two separate decisions: which identity a server presents, and which issuers a client trusts. An identity contains a certificate chain and its matching private key. A certificate authority (CA) signs certificates; clients must independently trust that authority before accepting its identities.

    The certificate package provisions local-development identities and imports existing client or server credentials as native Go TLS values. Keep generic application key management separate from certificate issuance. For file creation and trust-store command syntax, use Certificate Commands.

    Choose Trust

    For a public production service, obtain a certificate from an issuer your clients already trust, or terminate TLS at an ingress that manages those certificates. This package’s local CA is not publicly trusted and is not a production PKI, ACME client, or renewal service. Generating a certificate for a public hostname does not validate ownership of that name.

    For local HTTPS, generate a dedicated development CA and configure a particular client to trust its public certificate. A client-specific pool keeps that decision out of the operating system’s global trust store. Do not disable certificate verification to make local requests succeed: fix the trusted issuer, certificate names, or validity instead.

    The library neither writes files nor changes system trust. The examples below also do neither. If you later choose a CLI trust command, review its broader scope and required permissions separately.

    Provision Locally

    Add the independent module from your application’s module directory:

    go get arkoren.dev/foundation/certificate

    It works without Arkoren core. The server and retrying-client integrations use arkoren.dev/core; follow existing application installation if that dependency is not already present.

    Anyone with a trusted CA’s private key can issue identities accepted by its clients. Protect private exports and their storage directories, never commit them, and keep the CA key out of ordinary server deployments. Provision deliberately; do not regenerate missing credentials during application startup.

    In an explicit provisioning operation, import arkoren.dev/foundation/certificate and generate the development authority:

    authority, err := certificate.GenerateRoot("Reports Local Development")

    This creates an in-memory ECDSA P-256 root with a default lifetime of 5 × 365 days. After checking the error, issue a server identity for the names your local clients actually use:

    identity, err := certificate.GenerateServer(authority, []string{"localhost", "127.0.0.1", "::1"})

    The server certificate also uses P-256 and has server-authentication purpose. Its subject alternative names (SANs) are exactly the supplied DNS names and IP addresses; no localhost name is inferred. Supply bare ASCII DNS names or IP literals, not URLs or host-and-port pairs. A common name does not replace SANs.

    The default server lifetime is 90 days, shortened to the issuer’s remaining lifetime. To choose a shorter lifetime, import time and use a construction callback instead:

    identity, err := certificate.GenerateServer(authority, []string{"localhost"}, func(leaf *certificate.ServerBuilder) {
    	leaf.Validity(7 * 24 * time.Hour)
    })

    An explicit lifetime beyond the issuer’s expiry fails rather than being shortened. Zero selects the default; negative or positive subsecond lifetimes are invalid. RootBuilder.Validity similarly selects a root lifetime. The issuer must be a currently valid, self-signed, certificate-signing root with path-length-zero constraints, as generated above; intermediate CA issuance is not supported.

    identity.Bundle().PEM() exports the public chain, leaf first. identity.PrivatePEM() explicitly exports the unencrypted private key. Your provisioning operation owns secure persistence and distribution; the CLI provisioning commands offer exclusive file creation when you do not need custom storage. The package provides no automatic renewal or client-certificate generator.

    Load Identities

    During application startup, load previously provisioned credentials instead of generating replacements. With certificate imported, read the deployment’s server chain and private key:

    identity, err := certificate.LoadIdentity("server.pem", "server-key.pem")
    if err != nil {
    	return err
    }
    credentials, err := identity.TLS()
    if err != nil {
    	return err
    }

    These checks stop setup if files are missing or credentials cannot be converted. With crypto/tls imported, prepare the server’s native configuration:

    settings := &tls.Config{
    	MinVersion:   tls.VersionTLS12,
    	Certificates: []tls.Certificate{credentials},
    }

    Pass it to the server’s HTTPS setup before startup. The loader accepts RSA, ECDSA, and Ed25519 identities, not just generated P-256 keys. Chains are leaf-first and can omit the root. Private keys must be unencrypted PKCS #1, SEC 1, or PKCS #8 PEM. Each input or file read is bounded to 1 MiB; malformed blocks, extra keys, headers, and non-whitespace junk fail parsing. File errors preserve their OS causes.

    Parsing checks encoding, supplied chain order and signatures, and the key match. It does not establish trust, current validity, revocation, hostname coverage, or suitability for a particular TLS purpose. Peer verification during a native TLS handshake applies its policy; use x509.Certificate.Verify with explicit roots and options if you need a separate verification step.

    Choose Client Trust

    For a client that must trust only your private CA, obtain its public root through a trusted distribution path. Parsing a downloaded certificate does not establish its provenance. Load that public file, not the CA’s private key:

    roots, err := certificate.LoadBundle("development-ca.pem")
    if err != nil {
    	return err
    }
    pool, err := roots.Pool()
    if err != nil {
    	return err
    }

    Pool creates an independent private-only trust pool. It adds no system roots, so this pool does not also accept ordinary public issuers. With net/http, time, crypto/tls, and arkoren.dev/core/http/client imported, give the pool to a transport:

    transport, err := client.NewTransport(&tls.Config{
    	MinVersion: tls.VersionTLS12,
    	RootCAs:    pool,
    })
    if err != nil {
    	return err
    }
    outgoing := &http.Client{Transport: transport, Timeout: 8 * time.Second}

    Use this client with the request example. For the generated localhost identity, request https://localhost:8443 from a server configured to listen there. The URL must match a SAN; trusting the CA does not bypass name checks. RootCAs verifies the server and does not present a client identity.

    NewTransport clones the current default native transport, preserving proxy, pooling, and HTTP/2 settings. A default that is not a nonnil *http.Transport returns an error; for a base you already own, use custom transport ownership. When retiring the owned pool, call outgoing.CloseIdleConnections(); active requests continue.

    Add Public Roots

    If this client intentionally calls both private services and public services, replace roots.Pool() with an additive system pool:

    pool, err := roots.SystemPool()
    if err != nil {
    	return err
    }

    Use this pool in the same RootCAs field. It retains system roots and adds the bundle’s certificates to an independent pool; it does not alter the OS store. A system-root loading error is returned with its cause, not silently replaced by private-only trust. A zero bundle fails before system roots are loaded. Add only issuers you intend this client to trust.

    Require Client Identity

    Mutual TLS (mTLS) adds client-certificate verification to server verification. Obtain a client-purpose identity from your issuer; GenerateServer deliberately does not issue client identities. Load the supplied client chain and key with the same checked conversion:

    identity, err := certificate.LoadIdentity("client.pem", "client-key.pem")
    if err != nil {
    	return err
    }
    credentials, err := identity.TLS()
    if err != nil {
    	return err
    }

    On the client, use serverRoots, a pool of CAs trusted to issue server certificates, and present these credentials:

    transport, err := client.NewTransport(&tls.Config{
    	MinVersion:   tls.VersionTLS12,
    	RootCAs:      serverRoots,
    	Certificates: []tls.Certificate{credentials},
    })
    if err != nil {
    	return err
    }
    outgoing := &http.Client{Transport: transport, Timeout: 8 * time.Second}

    Sending a client identity does not replace verification of the server. Keep that separate trust policy and retire the owned client pool as above.

    On the server, use settings containing its own server identity from Load Identities. Before starting it, require verified client certificates with clientRoots, a pool made from the public client-issuing CA bundle:

    settings.ClientCAs = clientRoots
    settings.ClientAuth = tls.RequireAndVerifyClientCert

    ClientCAs and the outgoing client’s RootCAs have different roles and need not contain the same authorities. A missing, untrusted, expired, or wrong-purpose client certificate fails the handshake. After authentication, your application still decides which operations that identity may perform; trusting its CA does not grant unrestricted access.

    Share Safely

    Identity and Bundle are immutable. Native certificates, pools, PEM buffers, and TLS exports are detached, including the private key returned by TLS(). Changing an export does not change the original value. Their zero values are invalid; handle conversion errors or Err() before use. Do not log secret exports even though ordinary identity formatting is redacted.

    Once you place exported values into a native tls.Config, Go’s ownership rules apply. tls.Config.Clone is shallow: referenced pools, certificates, keys, and callback state remain shared. Finish configuration before sharing it with a running server or client, and do not mutate it afterward. Native certificate callbacks can select prepared identities; they do not provide automatic renewal.

    The retrying client’s default policy treats TLS errors as transport errors for eligible requests. Retrying does not repair bad trust or names. This differs from the CLI’s condition-wait policy, which treats certificate-verification failures as terminal.

    For parsing formats, generation constraints, and every export, see the certificate API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive