This commit refactors the certificate management implementation in the `certs` package such that multiple certificates can be specified at the same time. Therefore, the following layout of the `certs/` directory is expected: ``` certs/ │ ├─ public.crt ├─ private.key ├─ CAs/ // CAs directory is ignored │ │ │ ... │ ├─ example.com/ │ │ │ ├─ public.crt │ └─ private.key └─ foobar.org/ │ ├─ public.crt └─ private.key ... ``` However, directory names like `example.com` are just for human readability/organization and don't have any meaning w.r.t whether a particular certificate is served or not. This decision is made based on the SNI sent by the client and the SAN of the certificate. *** The `Manager` will pick a certificate based on the client trying to establish a TLS connection. In particular, it looks at the client hello (i.e. SNI) to determine which host the client tries to access. If the manager can find a certificate that matches the SNI it returns this certificate to the client. However, the client may choose to not send an SNI or tries to access a server directly via IP (`https://<ip>:<port>`). In this case, we cannot use the SNI to determine which certificate to serve. However, we also should not pick "the first" certificate that would be accepted by the client (based on crypto. parameters - like a signature algorithm) because it may be an internal certificate that contains internal hostnames. We would disclose internal infrastructure details doing so. Therefore, the `Manager` returns the "default" certificate when the client does not specify an SNI. The default certificate the top-level `public.crt` - i.e. `certs/public.crt`. This approach has some consequences: - It's the operator's responsibility to ensure that the top-level `public.crt` does not disclose any information (i.e. hostnames) that are not publicly visible. However, this was the case in the past already. - Any other `public.crt` - except for the top-level one - must not contain any IP SAN. The reason for this restriction is that the Manager cannot match a SNI to an IP b/c the SNI is the server host name. The entire purpose of SNI is to indicate which host the client tries to connect to when multiple hosts run on the same IP. So, a client will not set the SNI to an IP. If we would allow IP SANs in a lower-level `public.crt` a user would expect that it is possible to connect to MinIO directly via IP address and that the MinIO server would pick "the right" certificate. However, the MinIO server cannot determine which certificate to serve, and therefore always picks the "default" one. This may lead to all sorts of confusing errors like: "It works if I use `https:instance.minio.local` but not when I use `https://10.0.2.1`. These consequences/limitations should be pointed out / explained in our docs in an appropriate way. However, the support for multiple certificates should not have any impact on how deployment with a single certificate function today. Co-authored-by: Harshavardhana <harsha@minio.io>
109 lines
2.8 KiB
Go
109 lines
2.8 KiB
Go
/*
|
|
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package certs_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"io"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/minio/minio/pkg/certs"
|
|
)
|
|
|
|
func updateCerts(crt, key string) {
|
|
// ignore error handling
|
|
crtSource, _ := os.Open(crt)
|
|
defer crtSource.Close()
|
|
crtDest, _ := os.Create("public.crt")
|
|
defer crtDest.Close()
|
|
io.Copy(crtDest, crtSource)
|
|
|
|
keySource, _ := os.Open(key)
|
|
defer keySource.Close()
|
|
keyDest, _ := os.Create("private.key")
|
|
defer keyDest.Close()
|
|
io.Copy(keyDest, keySource)
|
|
}
|
|
|
|
func TestNewManager(t *testing.T) {
|
|
ctx, cancelFn := context.WithCancel(context.Background())
|
|
defer cancelFn()
|
|
c, err := certs.NewManager(ctx, "public.crt", "private.key", tls.LoadX509KeyPair)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hello := &tls.ClientHelloInfo{}
|
|
gcert, err := c.GetCertificate(hello)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expectedCert, err := tls.LoadX509KeyPair("public.crt", "private.key")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(gcert.Certificate, expectedCert.Certificate) {
|
|
t.Error("certificate doesn't match expected certificate")
|
|
}
|
|
_, err = certs.NewManager(ctx, "public.crt", "new-private.key", tls.LoadX509KeyPair)
|
|
if err == nil {
|
|
t.Fatal("Expected to fail but got success")
|
|
}
|
|
}
|
|
|
|
func TestValidPairAfterWrite(t *testing.T) {
|
|
ctx, cancelFn := context.WithCancel(context.Background())
|
|
defer cancelFn()
|
|
expectedCert, err := tls.LoadX509KeyPair("new-public.crt", "new-private.key")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c, err := certs.NewManager(ctx, "public.crt", "private.key", tls.LoadX509KeyPair)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
updateCerts("new-public.crt", "new-private.key")
|
|
defer updateCerts("original-public.crt", "original-private.key")
|
|
|
|
// Wait for the write event..
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
hello := &tls.ClientHelloInfo{}
|
|
gcert, err := c.GetCertificate(hello)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(gcert.Certificate, expectedCert.Certificate) {
|
|
t.Error("certificate doesn't match expected certificate")
|
|
}
|
|
|
|
rInfo := &tls.CertificateRequestInfo{}
|
|
gcert, err = c.GetClientCertificate(rInfo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(gcert.Certificate, expectedCert.Certificate) {
|
|
t.Error("client certificate doesn't match expected certificate")
|
|
}
|
|
}
|