M1: single-node OpenTunnel-compatible server
HTTP API per the recorded contract, bridge WebSocket, SNI relay with pass-through tunnel TLS, local CA issuance, JSON store. Verified end-to-end with the official opentunnel CLI.
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
// localCA is the insecure built-in issuer for development and self-hosting,
|
||||
// the equivalent of ISSUER=local in the reference server. The CA and every
|
||||
// certificate it issues use ECDSA P-256, like the ACME-issued ones.
|
||||
type localCA struct {
|
||||
key *ecdsa.PrivateKey
|
||||
cert *x509.Certificate
|
||||
certPEM []byte
|
||||
}
|
||||
|
||||
func newLocalCA() (*localCA, error) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "tunnel local CA"},
|
||||
NotBefore: time.Now().Add(-time.Minute),
|
||||
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
|
||||
IsCA: true,
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &localCA{
|
||||
key: key,
|
||||
cert: cert,
|
||||
certPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadLocalCA(keyPEM, certPEM []byte) (*localCA, error) {
|
||||
keyBlock, _ := pem.Decode(keyPEM)
|
||||
if keyBlock == nil {
|
||||
return nil, errors.New("no PEM block in CA key")
|
||||
}
|
||||
key, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ecKey, ok := key.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("CA key is not ECDSA")
|
||||
}
|
||||
certBlock, _ := pem.Decode(certPEM)
|
||||
if certBlock == nil {
|
||||
return nil, errors.New("no PEM block in CA certificate")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(certBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &localCA{key: ecKey, cert: cert, certPEM: pem.EncodeToMemory(certBlock)}, nil
|
||||
}
|
||||
|
||||
func (ca *localCA) keyPEM() ([]byte, error) {
|
||||
der, err := x509.MarshalPKCS8PrivateKey(ca.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), nil
|
||||
}
|
||||
|
||||
// issue signs a certificate for names with a fresh P-256 key. Used for the
|
||||
// server's own API certificate.
|
||||
func (ca *localCA) issue(names ...string) (certPEM, keyPEM []byte, err error) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
der, err := ca.sign(&x509.Certificate{
|
||||
SerialNumber: newSerial(),
|
||||
Subject: pkix.Name{CommonName: names[0]},
|
||||
DNSNames: names,
|
||||
NotBefore: time.Now().Add(-time.Minute),
|
||||
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}, &key.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}),
|
||||
pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), nil
|
||||
}
|
||||
|
||||
// errCSRHostname reports a CSR whose names are not the tunnel's own.
|
||||
type errCSRHostname struct {
|
||||
provided string
|
||||
expected string
|
||||
}
|
||||
|
||||
func (e errCSRHostname) Error() string {
|
||||
return "CSR hostname " + e.provided + " does not match tunnel hostname " + e.expected
|
||||
}
|
||||
|
||||
// signCSR validates a tunnel CSR and issues the certificate. The rules mirror
|
||||
// the reference's job runner: the common name must be the tunnel hostname and
|
||||
// every DNS name must be the hostname or its wildcard.
|
||||
func (ca *localCA) signCSR(csrPEM, hostname string) (certPEM, chainPEM []byte, expiry time.Time, err error) {
|
||||
block, _ := pem.Decode([]byte(csrPEM))
|
||||
if block == nil || block.Type != "CERTIFICATE REQUEST" {
|
||||
return nil, nil, time.Time{}, errors.New("not a PEM certificate request")
|
||||
}
|
||||
csr, err := x509.ParseCertificateRequest(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, time.Time{}, err
|
||||
}
|
||||
if err := csr.CheckSignature(); err != nil {
|
||||
return nil, nil, time.Time{}, fmt.Errorf("CSR signature: %w", err)
|
||||
}
|
||||
if csr.Subject.CommonName != hostname {
|
||||
return nil, nil, time.Time{}, errCSRHostname{provided: csr.Subject.CommonName, expected: hostname}
|
||||
}
|
||||
for _, name := range csr.DNSNames {
|
||||
if name != hostname && name != "*."+hostname {
|
||||
return nil, nil, time.Time{}, errCSRHostname{provided: name, expected: hostname}
|
||||
}
|
||||
}
|
||||
expiry = time.Now().Add(90 * 24 * time.Hour)
|
||||
der, err := ca.sign(&x509.Certificate{
|
||||
SerialNumber: newSerial(),
|
||||
Subject: pkix.Name{CommonName: hostname},
|
||||
DNSNames: csr.DNSNames,
|
||||
NotBefore: time.Now().Add(-time.Minute),
|
||||
NotAfter: expiry,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}, csr.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, time.Time{}, err
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), ca.certPEM, expiry, nil
|
||||
}
|
||||
|
||||
func (ca *localCA) sign(template *x509.Certificate, pub any) ([]byte, error) {
|
||||
return x509.CreateCertificate(rand.Reader, template, ca.cert, pub, ca.key)
|
||||
}
|
||||
|
||||
var serialLimit = new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
|
||||
func newSerial() *big.Int {
|
||||
n, err := rand.Int(rand.Reader, serialLimit)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
Reference in New Issue
Block a user