From 3da30e30422aaeaf65adff5a2a11b8f0d9bfdd25 Mon Sep 17 00:00:00 2001 From: Tim Lee Date: Thu, 11 Dec 2025 09:44:29 -0700 Subject: [PATCH] Android test MTLS server (#37030) --- tools/android/cert-auth-server/README.md | 88 ++++++++++++++++ tools/android/cert-auth-server/main.go | 129 +++++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 tools/android/cert-auth-server/README.md create mode 100644 tools/android/cert-auth-server/main.go diff --git a/tools/android/cert-auth-server/README.md b/tools/android/cert-auth-server/README.md new file mode 100644 index 0000000000..e272d6aaff --- /dev/null +++ b/tools/android/cert-auth-server/README.md @@ -0,0 +1,88 @@ +# Android Certificate Authentication Test Server + +A simple mTLS (mutual TLS) test server for validating Android device certificate-based authentication with Fleet. + +## Overview + +This server validates client certificates issued via SCEP (Simple Certificate Enrollment Protocol) to Android devices enrolled in Fleet. It demonstrates the end-to-end flow of: + +1. Fleet managing Android devices +2. SCEP server issuing device certificates +3. Devices authenticating to resources using those certificates + +## Prerequisites + +- Go 1.21+ +- [micromdm/scep](https://github.com/micromdm/scep) server +- Fleet server with Android MDM enabled + +## Quick Start + +### 1. Set Up the SCEP Server + +First, install and configure the micromdm/scep server to issue certificates to your Android devices. + +#### Install SCEP Server + +```bash +# Download from releases +curl -LO https://github.com/micromdm/scep/releases/latest/download/scepserver-darwin-arm64 +``` + +#### Initialize the CA + +```bash +./scepserver ca -init \ + -organization "Your Organization" \ + -country "US" \ + -common_name "Fleet SCEP CA" +``` + +This creates a `depot/` directory containing: + +- `ca.pem` - CA certificate +- `ca.key` - CA private key + +#### Start the SCEP Server + +```bash +./scepserver -depot depot -port 2016 -challenge=your-secret-challenge +``` + +The SCEP endpoint will be available at `http://localhost:2016/scep`. + +### 2. Configure Fleet for SCEP + +Configure Fleet to use your SCEP server for Android certificate enrollment. Add the SCEP configuration to your Fleet server: + +```yaml +# fleet.yml +mdm: + android: + scep_url: "http://your-scep-server:2016/scep" + scep_challenge: "your-secret-challenge" +``` + +Fleet will automatically request certificates for enrolled Android devices through the SCEP protocol. + +### 3. Run the Certificate Auth Server + +Build and run this test server, pointing it to the same CA that your SCEP server uses: + +```bash +# Build +go build -o cert-auth-server main.go + +# Run (using the CA certificate from your SCEP depot) +./cert-auth-server -ca-cert /path/to/depot/ca.pem -addr :8443 +``` + +### 4. Test Device Authentication + +From an enrolled Android device with a certificate issued by your SCEP server: + +Load the server URL in a browser or HTTP client: + +`https://your-cert-auth-server:8443/` + +It should prompt for a client certificate. Upon successful authentication, you should see a message confirming the device's identity. \ No newline at end of file diff --git a/tools/android/cert-auth-server/main.go b/tools/android/cert-auth-server/main.go new file mode 100644 index 0000000000..615358eda3 --- /dev/null +++ b/tools/android/cert-auth-server/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "fmt" + "io" + "log" + "math/big" + "net" + "net/http" + "os" + "time" +) + +func main() { + caCertPath := flag.String("ca-cert", "ca.crt", "Path to CA certificate used to verify client certificates") + addr := flag.String("addr", ":8443", "Address to listen on") + flag.Parse() + + // --- Load CA for validating client certificates --- + caCertBytes, err := os.ReadFile(*caCertPath) + if err != nil { + log.Fatalf("Error reading CA certificate (%s): %v", *caCertPath, err) + } + + caPool := x509.NewCertPool() + if !caPool.AppendCertsFromPEM(caCertBytes) { + log.Fatalf("Failed to append CA certificate at %s", *caCertPath) + } + + // --- Always generate a self-signed server certificate --- + serverCert, err := generateSelfSignedCert([]string{"localhost", "127.0.0.1"}) + if err != nil { + log.Fatalf("Error generating self-signed certificate: %v", err) + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + ClientCAs: caPool, + ClientAuth: tls.RequireAndVerifyClientCert, + MinVersion: tls.VersionTLS12, + } + + http.HandleFunc("/", mtlsHandler) + + srv := &http.Server{ + Addr: *addr, + TLSConfig: tlsConfig, + ReadHeaderTimeout: 10 * time.Second, + } + + log.Printf("mTLS server listening at https://%s (self-signed server cert, client CA=%s)", *addr, *caCertPath) + log.Fatal(srv.ListenAndServeTLS("", "")) +} + +func mtlsHandler(w http.ResponseWriter, r *http.Request) { + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { + http.Error(w, "Client certificate required", http.StatusUnauthorized) + return + } + + client := r.TLS.PeerCertificates[0] + + resp := fmt.Sprintf(` +

mTLS Authentication Successful

+

Client CN: %s

+

Issuer: %s

+

Subject: %s

+`, + client.Subject.CommonName, + client.Issuer.String(), + client.Subject.String(), + ) + + w.Header().Set("Content-Type", "text/html") + _, err := io.WriteString(w, resp) + if err != nil { + log.Printf("Error writing response: %v", err) + } +} + +func generateSelfSignedCert(hosts []string) (tls.Certificate, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return tls.Certificate{}, fmt.Errorf("generate rsa key: %w", err) + } + + serial, err := rand.Int(rand.Reader, big.NewInt(1<<62)) + if err != nil { + return tls.Certificate{}, fmt.Errorf("serial: %w", err) + } + + template := x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: "mtls-test-server", + }, + NotBefore: time.Now().Add(-1 * time.Minute), + NotAfter: time.Now().Add(24 * time.Hour), + + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return tls.Certificate{}, fmt.Errorf("create cert: %w", err) + } + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + + return tls.X509KeyPair(certPEM, keyPEM) +}