From 135bd95f8ca155765927661dd090ae253f1147a8 Mon Sep 17 00:00:00 2001 From: Wito Chandra Date: Tue, 11 Feb 2025 19:28:27 +0700 Subject: [PATCH 1/3] feat(api-client): add cert auth method --- api/auth/cert/cert.go | 163 ++++++++++++++++++++++++++++ api/auth/cert/cert_test.go | 213 +++++++++++++++++++++++++++++++++++++ api/auth/cert/go.mod | 28 +++++ api/auth/cert/go.sum | 79 ++++++++++++++ 4 files changed, 483 insertions(+) create mode 100644 api/auth/cert/cert.go create mode 100644 api/auth/cert/cert_test.go create mode 100644 api/auth/cert/go.mod create mode 100644 api/auth/cert/go.sum diff --git a/api/auth/cert/cert.go b/api/auth/cert/cert.go new file mode 100644 index 000000000000..372bf1f659d8 --- /dev/null +++ b/api/auth/cert/cert.go @@ -0,0 +1,163 @@ +package cert + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/hashicorp/go-rootcerts" + "github.com/hashicorp/vault/api" +) + +type CertAuth struct { + role string + caCert string + caCertBytes []byte + clientCert string + clientKey string + insecureSkipVerify bool +} + +var _ api.AuthMethod = (*CertAuth)(nil) + +type LoginOption func(a *CertAuth) error + +// NewCertAuth initializes a new cert auth method interface to be +// passed as a parameter to the client.Auth().Login method. +// +// Supported options: WithClientCertAndKey, WithInsecure +// +// https://developer.hashicorp.com/vault/api-docs/auth/cert#login-with-tls-certificate-method +func NewCertAuth(roleName string, opts ...LoginOption) (*CertAuth, error) { + a := &CertAuth{ + role: roleName, + } + + for _, opt := range opts { + err := opt(a) + if err != nil { + return nil, fmt.Errorf("error with login option: %w", err) + } + } + + return a, nil +} + +// Login sets up the required request body for the cert auth method's /login +// endpoint, and performs a write to it. +// It adds the client cert and key to the request. +func (a *CertAuth) Login(ctx context.Context, client *api.Client) (*api.Secret, error) { + if ctx == nil { + ctx = context.Background() + } + + c, err := a.httpClient() + if err != nil { + return nil, err + } + + reqBody, err := json.Marshal(map[string]interface{}{ + "name": a.role, + }) + if err != nil { + return nil, fmt.Errorf("unable to marshal login data: %w", err) + } + + url := fmt.Sprintf("%s/v1/auth/cert/login", client.Address()) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(reqBody)) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + resp, err := c.Do(req) + if err != nil { + return nil, fmt.Errorf("unable to log in with cert auth: %w", err) + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unable to read response body: %w", err) + } + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("unable to log in with cert auth, response code: %d. response body: %s", resp.StatusCode, string(body)) + } + + var secret api.Secret + if err := json.Unmarshal(body, &secret); err != nil { + return nil, fmt.Errorf("unable to unmarshal response body: %w", err) + } + + return &secret, nil +} + +func (a *CertAuth) httpClient() (*http.Client, error) { + cert, err := tls.LoadX509KeyPair(a.clientCert, a.clientKey) + if err != nil { + return nil, fmt.Errorf("unable to load cert: %w", err) + } + + tlsConfig := &tls.Config{ + InsecureSkipVerify: a.insecureSkipVerify, + Certificates: []tls.Certificate{cert}, + } + + if a.caCert != "" || len(a.caCertBytes) > 0 { + err = rootcerts.ConfigureTLS(tlsConfig, &rootcerts.Config{ + CAPath: a.caCert, + CACertificate: a.caCertBytes, + }) + + if err != nil { + return nil, fmt.Errorf("unable to configure TLS: %w", err) + } + } + + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + }, nil +} + +// WithCACert sets the CA cert to be used for the login request. +// caCert is the path to the CA cert file. +func WithCACert(caCert string) LoginOption { + return func(a *CertAuth) error { + a.caCert = caCert + return nil + } +} + +// WithCACertBytes sets the CA cert to be used for the login request. +// caCertBytes is the bytes of the CA cert. +// caCertBytes takes precedence over caCert. +func WithCACertBytes(caCertBytes []byte) LoginOption { + return func(a *CertAuth) error { + a.caCertBytes = caCertBytes + return nil + } +} + +// WithClientCertAndKey sets the client cert and key to be used for the login request. +func WithClientCertAndKey(clientCert, clientKey string) LoginOption { + return func(a *CertAuth) error { + a.clientCert = clientCert + a.clientKey = clientKey + return nil + } +} + +// WithInsecure skips the verification of the server's certificate chain and host name. +func WithInsecure() LoginOption { + return func(a *CertAuth) error { + a.insecureSkipVerify = true + return nil + } +} diff --git a/api/auth/cert/cert_test.go b/api/auth/cert/cert_test.go new file mode 100644 index 000000000000..235854cd1481 --- /dev/null +++ b/api/auth/cert/cert_test.go @@ -0,0 +1,213 @@ +package cert + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + mathrand "math/rand" + "net" + "net/http" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/hashicorp/go-rootcerts" + "github.com/hashicorp/vault/api" +) + +func TestLogin(t *testing.T) { + certDir := createCertificatePairs(t) + + wg := &sync.WaitGroup{} + wg.Add(1) + ln, sv := runTestServer(t, certDir, func(w http.ResponseWriter, r *http.Request) { + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { + t.Fatalf("no client cert provided") + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"auth": {"client_token": "test-token"}}`)) + + wg.Done() + }) + defer sv.Shutdown(context.Background()) + + // Create a new CertAuth struct. + auth, err := NewCertAuth( + "role-name", + WithCACert(filepath.Join(certDir, "ca_cert.pem")), + WithClientCertAndKey(filepath.Join(certDir, "client_cert.pem"), filepath.Join(certDir, "client_key.pem")), + ) + + cfg := api.DefaultConfig() + cfg.Address = fmt.Sprintf("https://%s", ln.Addr()) + + client, err := api.NewClient(cfg) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + _, err = auth.Login(context.Background(), client) + if err != nil { + t.Fatalf("failed to login: %v", err) + } + + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + t.Log("done") + case <-time.After(time.Second): + t.Error("timeout") + } +} + +func runTestServer(t *testing.T, certDir string, fn http.HandlerFunc) (net.Listener, *http.Server) { + certPool, err := rootcerts.LoadCACerts(&rootcerts.Config{ + CAPath: filepath.Join(certDir, "ca_cert.pem"), + }) + if err != nil { + t.Fatalf("failed to load CA certs: %v", err) + } + + tlsConfig := &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: certPool, + RootCAs: certPool, + } + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("err: %s", err) + } + + sv := &http.Server{ + TLSConfig: tlsConfig, + Handler: http.HandlerFunc(fn), + } + + go func() { + sv.ServeTLS(ln, filepath.Join(certDir, "server_cert.pem"), filepath.Join(certDir, "server_key.pem")) + }() + + return ln, sv +} + +func createCertificatePairs(t *testing.T) string { + tempDir, err := os.MkdirTemp("", "api_certauth_test") + if err != nil { + t.Fatal(err) + } + + t.Logf("test %s, temp dir %s", t.Name(), tempDir) + caCertTemplate := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "localhost", + }, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), + SerialNumber: big.NewInt(mathrand.Int63()), + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: time.Now().Add(262980 * time.Hour), + BasicConstraintsValid: true, + IsCA: true, + } + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + caBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, caKey.Public(), caKey) + if err != nil { + t.Fatal(err) + } + caCert, err := x509.ParseCertificate(caBytes) + if err != nil { + t.Fatal(err) + } + caCertPEMBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + } + err = os.WriteFile(filepath.Join(tempDir, "ca_cert.pem"), pem.EncodeToMemory(caCertPEMBlock), 0o755) + if err != nil { + t.Fatal(err) + } + marshaledCAKey, err := x509.MarshalECPrivateKey(caKey) + if err != nil { + t.Fatal(err) + } + caKeyPEMBlock := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: marshaledCAKey, + } + err = os.WriteFile(filepath.Join(tempDir, "ca_key.pem"), pem.EncodeToMemory(caKeyPEMBlock), 0o755) + if err != nil { + t.Fatal(err) + } + + createCertificate := func(prefix string) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + template := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "example.com", + }, + EmailAddresses: []string{"valid@example.com"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, + SerialNumber: big.NewInt(mathrand.Int63()), + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: time.Now().Add(262980 * time.Hour), + } + certBytes, err := x509.CreateCertificate(rand.Reader, template, caCert, key.Public(), caKey) + if err != nil { + t.Fatal(err) + } + certPEMBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + } + err = os.WriteFile(filepath.Join(tempDir, prefix+"_cert.pem"), pem.EncodeToMemory(certPEMBlock), 0o755) + if err != nil { + t.Fatal(err) + } + marshaledKey, err := x509.MarshalECPrivateKey(key) + if err != nil { + t.Fatal(err) + } + keyPEMBlock := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: marshaledKey, + } + err = os.WriteFile(filepath.Join(tempDir, prefix+"_key.pem"), pem.EncodeToMemory(keyPEMBlock), 0o755) + if err != nil { + t.Fatal(err) + } + } + + createCertificate("client") + createCertificate("server") + + return tempDir +} diff --git a/api/auth/cert/go.mod b/api/auth/cert/go.mod new file mode 100644 index 000000000000..66212104c326 --- /dev/null +++ b/api/auth/cert/go.mod @@ -0,0 +1,28 @@ +module github.com/hashicorp/vault/api/auth/cert + +go 1.21 + +require ( + github.com/hashicorp/go-rootcerts v1.0.2 + github.com/hashicorp/vault/api v1.16.0 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect +) diff --git a/api/auth/cert/go.sum b/api/auth/cert/go.sum new file mode 100644 index 000000000000..4c87563bd72e --- /dev/null +++ b/api/auth/cert/go.sum @@ -0,0 +1,79 @@ +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= +github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= +github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 03389f06087493c238366cfee08220aeb5562637 Mon Sep 17 00:00:00 2001 From: Wito Chandra Date: Thu, 13 Feb 2025 19:03:29 +0700 Subject: [PATCH 2/3] chore: apply feedbacks --- api/auth/cert/cert.go | 36 +++++++++++++++++++----------------- api/auth/cert/cert_test.go | 30 +++++++++++++++++++++--------- changelog/29546.txt | 3 +++ 3 files changed, 43 insertions(+), 26 deletions(-) create mode 100644 changelog/29546.txt diff --git a/api/auth/cert/cert.go b/api/auth/cert/cert.go index 372bf1f659d8..64efdf20375c 100644 --- a/api/auth/cert/cert.go +++ b/api/auth/cert/cert.go @@ -26,28 +26,40 @@ var _ api.AuthMethod = (*CertAuth)(nil) type LoginOption func(a *CertAuth) error -// NewCertAuth initializes a new cert auth method interface to be +// NewCertAuth initializes a new Cert auth method interface to be // passed as a parameter to the client.Auth().Login method. // -// Supported options: WithClientCertAndKey, WithInsecure -// -// https://developer.hashicorp.com/vault/api-docs/auth/cert#login-with-tls-certificate-method -func NewCertAuth(roleName string, opts ...LoginOption) (*CertAuth, error) { +// Supported options: WithCACert, WithCACertBytes, WithInsecure +func NewCertAuth(roleName, clientCert, clientKey string, opts ...LoginOption) (*CertAuth, error) { + if roleName == "" { + return nil, fmt.Errorf("no role name provided for login") + } + + if clientCert == "" || clientKey == "" { + return nil, fmt.Errorf("client certificate and key must be provided") + } + a := &CertAuth{ - role: roleName, + role: roleName, + clientCert: clientCert, + clientKey: clientKey, } + // Loop through each option for _, opt := range opts { + // Call the option giving the instantiated + // *CertAuth as the argument err := opt(a) if err != nil { return nil, fmt.Errorf("error with login option: %w", err) } } + // return the modified auth struct instance return a, nil } -// Login sets up the required request body for the cert auth method's /login +// Login sets up the required request body for the Cert auth method's /login // endpoint, and performs a write to it. // It adds the client cert and key to the request. func (a *CertAuth) Login(ctx context.Context, client *api.Client) (*api.Secret, error) { @@ -113,7 +125,6 @@ func (a *CertAuth) httpClient() (*http.Client, error) { CAPath: a.caCert, CACertificate: a.caCertBytes, }) - if err != nil { return nil, fmt.Errorf("unable to configure TLS: %w", err) } @@ -145,15 +156,6 @@ func WithCACertBytes(caCertBytes []byte) LoginOption { } } -// WithClientCertAndKey sets the client cert and key to be used for the login request. -func WithClientCertAndKey(clientCert, clientKey string) LoginOption { - return func(a *CertAuth) error { - a.clientCert = clientCert - a.clientKey = clientKey - return nil - } -} - // WithInsecure skips the verification of the server's certificate chain and host name. func WithInsecure() LoginOption { return func(a *CertAuth) error { diff --git a/api/auth/cert/cert_test.go b/api/auth/cert/cert_test.go index 235854cd1481..6bb9c6b47d5d 100644 --- a/api/auth/cert/cert_test.go +++ b/api/auth/cert/cert_test.go @@ -29,24 +29,27 @@ func TestLogin(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - ln, sv := runTestServer(t, certDir, func(w http.ResponseWriter, r *http.Request) { + ln := runTestServer(t, certDir, func(w http.ResponseWriter, r *http.Request) { + wg.Done() + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { t.Fatalf("no client cert provided") } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"auth": {"client_token": "test-token"}}`)) - - wg.Done() }) - defer sv.Shutdown(context.Background()) // Create a new CertAuth struct. auth, err := NewCertAuth( "role-name", + filepath.Join(certDir, "client_cert.pem"), + filepath.Join(certDir, "client_key.pem"), WithCACert(filepath.Join(certDir, "ca_cert.pem")), - WithClientCertAndKey(filepath.Join(certDir, "client_cert.pem"), filepath.Join(certDir, "client_key.pem")), ) + if err != nil { + t.Fatalf("failed to create CertAuth: %v", err) + } cfg := api.DefaultConfig() cfg.Address = fmt.Sprintf("https://%s", ln.Addr()) @@ -56,11 +59,15 @@ func TestLogin(t *testing.T) { t.Fatalf("failed to create client: %v", err) } - _, err = auth.Login(context.Background(), client) + secret, err := auth.Login(context.Background(), client) if err != nil { t.Fatalf("failed to login: %v", err) } + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken != "test-token" { + t.Fatalf("unexpected response: %v", secret) + } + done := make(chan struct{}) go func() { wg.Wait() @@ -75,7 +82,7 @@ func TestLogin(t *testing.T) { } } -func runTestServer(t *testing.T, certDir string, fn http.HandlerFunc) (net.Listener, *http.Server) { +func runTestServer(t *testing.T, certDir string, fn http.HandlerFunc) net.Listener { certPool, err := rootcerts.LoadCACerts(&rootcerts.Config{ CAPath: filepath.Join(certDir, "ca_cert.pem"), }) @@ -91,7 +98,7 @@ func runTestServer(t *testing.T, certDir string, fn http.HandlerFunc) (net.Liste ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { - t.Fatalf("err: %s", err) + t.Fatalf("failed to create listener: %v", err) } sv := &http.Server{ @@ -99,11 +106,16 @@ func runTestServer(t *testing.T, certDir string, fn http.HandlerFunc) (net.Liste Handler: http.HandlerFunc(fn), } + t.Cleanup(func() { + sv.Close() + ln.Close() + }) + go func() { sv.ServeTLS(ln, filepath.Join(certDir, "server_cert.pem"), filepath.Join(certDir, "server_key.pem")) }() - return ln, sv + return ln } func createCertificatePairs(t *testing.T) string { diff --git a/changelog/29546.txt b/changelog/29546.txt new file mode 100644 index 000000000000..fd71f36a1729 --- /dev/null +++ b/changelog/29546.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +api/client: Add Cert auth method support. This allows the client to authenticate using a client certificate. +``` From 77a33097ecaeea2b0e93f4be6b3a837cb6a87a63 Mon Sep 17 00:00:00 2001 From: Wito Chandra Date: Fri, 14 Feb 2025 20:42:38 +0700 Subject: [PATCH 3/3] doc: add copyright & update changelog --- api/auth/cert/cert.go | 3 +++ api/auth/cert/cert_test.go | 3 +++ changelog/29546.txt | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/api/auth/cert/cert.go b/api/auth/cert/cert.go index 64efdf20375c..c0d983cfcdb9 100644 --- a/api/auth/cert/cert.go +++ b/api/auth/cert/cert.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package cert import ( diff --git a/api/auth/cert/cert_test.go b/api/auth/cert/cert_test.go index 6bb9c6b47d5d..97a900e855c6 100644 --- a/api/auth/cert/cert_test.go +++ b/api/auth/cert/cert_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package cert import ( diff --git a/changelog/29546.txt b/changelog/29546.txt index fd71f36a1729..2b7c076c1c89 100644 --- a/changelog/29546.txt +++ b/changelog/29546.txt @@ -1,3 +1,3 @@ -```release-note:enhancement +```release-note:improvement api/client: Add Cert auth method support. This allows the client to authenticate using a client certificate. ```