ratls

package module
v0.0.0-...-d2a14b3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 27, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

README

RA-TLS Issuer for Caddy

AGPLv3 License

A Caddy tls.issuance module (ra_tls) that produces RA-TLS certificates for Confidential Computing.

Extended certificate

This module will serve an x509 certificate extended with an attribute for the TEE quote. For exemple, for an Intel TDX Confidential VM:

image

Supported Backends

Backend Hardware Status
tdx Intel TDX Supported
sev-snp AMD SEV-SNP Planned

The hardware-specific logic is abstracted behind the Attester interface (attester.go). Each backend lives in its own file (e.g. attester_tdx.go) and registers itself via init().

How It Works

The module supports two attestation paths:

Deterministic (Issue path)
  1. Key Generation — An ECDSA P-256 key pair is generated inside the TEE.
  2. Attestation — We chose that ReportData = SHA-512( SHA-256(DER public key) || creation_time ), where creation_time is NotBefore truncated to 1-minute precision ("2006-01-02T15:04Z"). The quote is obtained from the configured backend.
  3. Certificate — An X.509 certificate is signed by a user-provided intermediary CA (private PKI), embedding the attestation evidence in a backend-specific extension OID. The PEM output includes the full chain (leaf + CA cert).

Certificates are cached by certmagic and auto-renewed. A verifier reproduces the ReportData from the certificate alone.

Challenge-Response (GetCertificate path)

When a TLS client sends a RA-TLS challenge in its ClientHello (extension 0xffbb):

  1. Ephemeral Key — A fresh ECDSA P-256 key pair is generated.
  2. AttestationReportData = SHA-512( SHA-256(DER public key) || nonce ), binding the quote to the client's challenge.
  3. Certificate — A very short-lived (5 min) certificate is signed by the same CA, with the quote embedded.

This certificate is not cached — each challenge produces a unique response.

Note: This branch requires the Privasys/go fork (initial commit) which adds ClientHelloInfo.RATLSChallenge to crypto/tls, enabling challenge-response attestation. An upstream PR is open at golang/go#77714. The code references hello.RATLSChallenge directly and will not compile with standard Go.

Requirements

  • Linux host running inside a Confidential VM
  • Backend-specific support:
    • tdx — Kernel configfs-tsm (/sys/kernel/config/tsm/report)
  • An intermediary CA certificate and private key (private PKI)
  • Privasys/go fork (ratls branch — initial commit) and xcaddy

Building

First, build the Go fork:

git clone -b ratls https://github.com/Privasys/go.git ~/go-ratls
cd ~/go-ratls/src && ./make.bash
export GOROOT=~/go-ratls
export PATH=$GOROOT/bin:$PATH

Then build Caddy with the RA-TLS module:

go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

git clone https://github.com/Privasys/ra-tls-caddy.git
cd ra-tls-caddy
xcaddy build --with github.com/Privasys/ra-tls-caddy=.

Note: The =. suffix tells xcaddy to use the local directory as the module source. The import path before = must match the module directive in go.mod. If you are working from a fork, update go.mod accordingly.

Caddyfile Usage

example.com {
    tls {
        issuer ra_tls {
            backend tdx
            ca_cert /path/to/intermediate-ca.crt
            ca_key  /path/to/intermediate-ca.key
        }
    }
    respond "Hello from a Confidential VM!"
}

JSON Config Usage

{
  "apps": {
    "tls": {
      "automation": {
        "policies": [
          {
            "subjects": ["example.com"],
            "issuers": [
              {
                "module": "ra_tls",
                "backend": "tdx",
                "ca_cert_path": "/path/to/intermediate-ca.crt",
                "ca_key_path": "/path/to/intermediate-ca.key"
              }
            ]
          }
        ]
      }
    }
  }
}

Inspecting the Certificate

Once Caddy is running, inspect the RA-TLS certificate with standard tools:

# Retrieve and display the full certificate
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -text

Look for the TDX quote in the X.509 extensions:

X509v3 extensions:
    ...
    1.2.840.113741.1.5.5.1.6:
        <hex dump of the TDX quote — ~8000 bytes of attestation evidence>

To save the certificate for programmatic verification:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -outform PEM > ratls-cert.pem

openssl asn1parse -in ratls-cert.pem

Key Sensitivity

The ECDSA private key is generated inside the TEE and protected by hardware memory encryption at runtime. However, certmagic will PEM-encode and persist it via its storage backend. To prevent writing it to unencrypted disk:

  • Use an encrypted filesystem or volume for Caddy's data directory.
  • Or configure a secrets-manager storage module.

Verification

A relying party verifying an RA-TLS certificate should:

Deterministic path
  1. Validate the certificate chain back to the trusted root CA.
  2. Extract the attestation evidence from the backend-specific extension OID (e.g. 1.2.840.113741.1.5.5.1.6 for TDX).
  3. Verify the evidence against the hardware vendor's attestation infrastructure.
  4. Read the certificate's NotBefore field, format it as "2006-01-02T15:04Z" (UTC, minute precision).
  5. Compute SHA-512( SHA-256(DER public key) || formatted_time_string ) and confirm it matches the quote's ReportData.
  6. Check the quote's measurement registers against expected values.
Challenge-response path
  1. Validate the certificate chain back to the trusted root CA.
  2. Extract the attestation evidence from the backend-specific extension OID.
  3. Verify the evidence against the hardware vendor's attestation infrastructure.
  4. Compute SHA-512( SHA-256(DER public key) || original_nonce ) using the nonce sent in the ClientHello.
  5. Confirm the result matches the quote's ReportData — this proves freshness.
  6. Check the quote's measurement registers against expected values.

Adding a New Backend

  1. Create attester_<name>.go implementing the Attester interface.
  2. Register it in an init() function: RegisterAttester("<name>", func() Attester { return new(MyAttester) })
  3. The new backend is immediately available via backend <name> in the Caddyfile.

Third-Party Licenses & Acknowledgments

This project makes use of the following open source libraries:

Please refer to each project for their respective license terms.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Security

If you discover a security vulnerability, please report it responsibly. See SECURITY.md for details.

License

This project is licensed under the GNU Affero General Public License v3 (AGPL-3.0).

You are free to use, modify, and distribute this software under the terms of the AGPL-3.0. Any modified versions or services built on this software that are accessible over a network must make the complete source code available under the same license.

Commercial Licensing

For commercial, closed-source, or proprietary use that is not compatible with the AGPL-3.0, a separate commercial license is available.

Please contact legal@privasys.org for licensing enquiries.

Documentation

Overview

Package ratls implements a Caddy TLS issuance module ("ra_tls") that produces RA-TLS certificates for Confidential VMs.

It generates ECDSA P-256 key pairs and issues X.509 certificates signed by a private-PKI intermediary CA. Each certificate embeds hardware attestation evidence (a quote/report) in a custom X.509 extension whose OID is determined by the selected backend.

Backends

Hardware-specific logic is abstracted behind the Attester interface (see attester.go). The backend is selected via the "backend" configuration field. Currently supported:

  • "tdx" -- Intel TDX via Linux configfs-tsm (attester_tdx.go)
  • "sgx" -- Intel SGX via Gramine DCAP (attester_sgx.go)

Planned:

  • "sev-snp" -- AMD SEV-SNP

Report Data

The quote's 64-byte ReportData field is:

SHA-512( SHA-256(DER public key) || creation_time )

where creation_time is the certificate's NotBefore value truncated to 1-minute precision, formatted as the UTC string "2006-01-02T15:04Z". This allows a verifier to reproduce the ReportData from the certificate alone: read the public key and NotBefore, apply the same formula, and compare against the quote.

Trust Model

Certificates are signed by a user-provided intermediary CA (private PKI). The attestation evidence embedded in the certificate provides hardware- rooted proof that the public key was generated inside a genuine Confidential VM. A relying party should:

  1. Validate the certificate chain back to the trusted root CA.
  2. Extract and verify the attestation evidence against the hardware vendor's attestation infrastructure.
  3. Recompute SHA-512(SHA-256(pub key) || NotBefore as "2006-01-02T15:04Z") and confirm it matches the quote's ReportData.

Attestation Paths

The module supports two attestation modes:

  • Deterministic (Issue path): ReportData = SHA-512(SHA-256(pubkey) || time). Certificates are cached and auto-renewed by certmagic. A verifier reproduces the ReportData from the certificate's public key and NotBefore.

  • Challenge-Response (GetCertificate path): When the client's TLS ClientHello contains a RA-TLS challenge extension (0xffbb), a fresh ephemeral certificate is generated with ReportData = SHA-512(SHA-256(pubkey) || nonce). This certificate is not cached. To read the challenge payload, build with the Privasys/go fork (https://github.com/Privasys/go/tree/ratls) and the "ratls" build tag. With standard Go the extension is detected but the payload cannot be read, so the module falls back to the deterministic certificate.

Private Key Sensitivity

The ECDSA private key is generated inside the TEE and protected by hardware memory encryption. It should be treated as highly sensitive:

  • It is held in an in-memory sync.Map only between GenerateKey and Issue, then immediately deleted from the map after use.
  • certmagic will still PEM-encode and persist the key via its Storage backend. To avoid writing it to unencrypted disk, configure Caddy with an encrypted or in-memory storage backend.

Caddyfile Example

example.com {
   tls {
       issuer ra_tls {
           backend tdx
           ca_cert /path/to/intermediate-ca.crt
           ca_key  /path/to/intermediate-ca.key
       }
   }
   respond "Hello from a Confidential VM!"
}

Build

xcaddy build --with github.com/Privasys/ra-tls-caddy=.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterAttester

func RegisterAttester(name string, factory func() Attester)

RegisterAttester registers an Attester factory under the given name. Call this from an init() function in the backend's source file.

Types

type Attester

type Attester interface {
	// Name returns the short identifier for this attester (e.g. "tdx",
	// "sev-snp"). Used in log messages and issuer keys.
	Name() string

	// Provision initialises the attester, validating hardware availability
	// and setting up any providers. Called once during Caddy provisioning.
	Provision(logger *zap.Logger) error

	// Quote generates raw attestation evidence for the given 64-byte
	// report data.
	Quote(reportData [64]byte) ([]byte, error)

	// CertExtension returns the X.509 extension (OID + value) used to
	// embed the attestation evidence in an RA-TLS certificate. The raw
	// quote bytes are passed in as the extension value.
	CertExtension(quote []byte) pkix.Extension
}

Attester abstracts hardware-specific confidential computing attestation. Implementations produce attestation evidence (quotes/reports) for a given 64-byte ReportData value.

To add a new backend, implement this interface and register a factory via RegisterAttester in an init() function — typically in a file named attester_<backend>.go.

type RATLSIssuer

type RATLSIssuer struct {
	// Backend selects the confidential computing hardware backend.
	// Supported values: "tdx". Planned: "sev-snp", "sgx".
	Backend string `json:"backend"`

	// CACertPath is the path to the PEM-encoded intermediary CA certificate
	// used to sign issued RA-TLS certificates.
	CACertPath string `json:"ca_cert_path"`

	// CAKeyPath is the path to the PEM-encoded private key of the
	// intermediary CA.
	CAKeyPath string `json:"ca_key_path"`
	// contains filtered or unexported fields
}

RATLSIssuer is a Caddy TLS issuance module that produces RA-TLS certificates for Confidential VMs.

It implements:

  • certmagic.KeyGenerator -- generates ECDSA P-256 key pairs.
  • certmagic.Issuer -- issues CA-signed certificates with embedded attestation evidence.
  • certmagic.Manager -- serves challenge-response certs for RA-TLS clients.
  • caddy.Provisioner -- verifies hardware availability and loads the CA.
  • caddyfile.Unmarshaler -- parses the "ra_tls" Caddyfile directive.

func (RATLSIssuer) CaddyModule

func (RATLSIssuer) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*RATLSIssuer) GenerateKey

func (iss *RATLSIssuer) GenerateKey() (crypto.PrivateKey, error)

GenerateKey generates an ECDSA P-256 key pair and stores it temporarily so that the subsequent call to Issue can retrieve it for signing by the intermediary CA.

The returned crypto.PrivateKey is an *ecdsa.PrivateKey (which also implements crypto.Signer). Because the key is generated inside a TEE, it should be treated as sensitive -- configure an encrypted or in-memory storage backend in Caddy to avoid persisting it in plaintext.

func (*RATLSIssuer) GetCertificate

func (iss *RATLSIssuer) GetCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (*tls.Certificate, error)

GetCertificate implements certmagic.Manager. It inspects the TLS ClientHello for a RA-TLS challenge extension (0xffbb). If present and the challenge payload is available (requires the Privasys/go fork + "ratls" build tag), it generates a fresh ephemeral certificate with attestation evidence bound to the client's nonce — providing interactive, challenge-response attestation.

If no RA-TLS challenge is found, or if the extension is detected but the payload is unavailable (standard Go), it returns (nil, nil) to let certmagic serve a pre-issued certificate from the Issue() path.

func (*RATLSIssuer) Issue

Issue creates a CA-signed X.509 certificate with embedded attestation evidence from the configured hardware backend.

Steps performed:

  1. Validate the CSR signature.
  2. Determine the creation time (NotBefore), truncated to 1-minute precision.
  3. Compute ReportData = SHA-512( SHA-256(DER public key) || creation_time ).
  4. Request attestation evidence from the backend with the ReportData.
  5. Build a certificate template carrying the evidence in a backend- specific extension and sign it with the intermediary CA key.

func (*RATLSIssuer) IssuerKey

func (iss *RATLSIssuer) IssuerKey() string

IssuerKey returns a string that uniquely identifies this issuer configuration, used by certmagic to namespace stored certificates.

func (*RATLSIssuer) Provision

func (iss *RATLSIssuer) Provision(ctx caddy.Context) error

Provision validates configuration, loads the intermediary CA, and initialises the hardware-specific attestation backend.

func (*RATLSIssuer) UnmarshalCaddyfile

func (iss *RATLSIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile parses the "ra_tls" issuer directive:

tls {
   issuer ra_tls {
       backend  tdx
       ca_cert  /path/to/intermediate-ca.crt
       ca_key   /path/to/intermediate-ca.key
   }
}

type TDXAttester

type TDXAttester struct {
	// contains filtered or unexported fields
}

TDXAttester implements Attester for Intel TDX Confidential VMs using the Linux configfs-tsm interface.

func (*TDXAttester) CertExtension

func (a *TDXAttester) CertExtension(quote []byte) pkix.Extension

CertExtension returns a non-critical X.509 extension with the TDX quote under OID 1.2.840.113741.1.5.5.1.6.

func (*TDXAttester) Name

func (a *TDXAttester) Name() string

Name returns "tdx".

func (*TDXAttester) Provision

func (a *TDXAttester) Provision(logger *zap.Logger) error

Provision checks for /sys/kernel/config/tsm/report, initialises the configfs-tsm quote provider, and verifies that it is supported.

func (*TDXAttester) Quote

func (a *TDXAttester) Quote(reportData [64]byte) ([]byte, error)

Quote obtains a TDX attestation quote via configfs-tsm for the given 64-byte report data.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL