mirror of
https://github.com/status-im/consul.git
synced 2025-01-09 13:26:07 +00:00
5fb9df1640
* Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
265 lines
6.5 KiB
Go
265 lines
6.5 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package tlsutil
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"time"
|
|
|
|
"net/url"
|
|
|
|
"github.com/hashicorp/consul/agent/connect"
|
|
)
|
|
|
|
// GenerateSerialNumber returns random bigint generated with crypto/rand
|
|
func GenerateSerialNumber() (*big.Int, error) {
|
|
l := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
s, err := rand.Int(rand.Reader, l)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// GeneratePrivateKey generates a new ecdsa private key
|
|
func GeneratePrivateKey() (crypto.Signer, string, error) {
|
|
return connect.GeneratePrivateKey()
|
|
}
|
|
|
|
type CAOpts struct {
|
|
Signer crypto.Signer
|
|
Serial *big.Int
|
|
ClusterID string
|
|
Days int
|
|
PermittedDNSDomains []string
|
|
Domain string
|
|
Name string
|
|
}
|
|
|
|
type CertOpts struct {
|
|
Signer crypto.Signer
|
|
CA string
|
|
Serial *big.Int
|
|
Name string
|
|
Days int
|
|
DNSNames []string
|
|
IPAddresses []net.IP
|
|
ExtKeyUsage []x509.ExtKeyUsage
|
|
IsCA bool
|
|
}
|
|
|
|
// GenerateCA generates a new CA for agent TLS (not to be confused with Connect TLS)
|
|
func GenerateCA(opts CAOpts) (string, string, error) {
|
|
signer := opts.Signer
|
|
var pk string
|
|
if signer == nil {
|
|
var err error
|
|
signer, pk, err = GeneratePrivateKey()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
id, err := keyID(signer.Public())
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
sn := opts.Serial
|
|
if sn == nil {
|
|
var err error
|
|
sn, err = GenerateSerialNumber()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
name := opts.Name
|
|
if name == "" {
|
|
name = fmt.Sprintf("Consul Agent CA %d", sn)
|
|
}
|
|
|
|
days := opts.Days
|
|
if opts.Days == 0 {
|
|
days = 365
|
|
}
|
|
|
|
var uris []*url.URL
|
|
if opts.ClusterID != "" {
|
|
spiffeID := connect.SpiffeIDSigning{ClusterID: opts.ClusterID, Domain: opts.Domain}
|
|
uris = []*url.URL{spiffeID.URI()}
|
|
}
|
|
|
|
// Create the CA cert
|
|
template := x509.Certificate{
|
|
SerialNumber: sn,
|
|
URIs: uris,
|
|
Subject: pkix.Name{
|
|
Country: []string{"US"},
|
|
PostalCode: []string{"94105"},
|
|
Province: []string{"CA"},
|
|
Locality: []string{"San Francisco"},
|
|
StreetAddress: []string{"101 Second Street"},
|
|
Organization: []string{"HashiCorp Inc."},
|
|
CommonName: name,
|
|
},
|
|
BasicConstraintsValid: true,
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
|
|
IsCA: true,
|
|
NotAfter: time.Now().AddDate(0, 0, days),
|
|
NotBefore: time.Now(),
|
|
AuthorityKeyId: id,
|
|
SubjectKeyId: id,
|
|
}
|
|
|
|
if len(opts.PermittedDNSDomains) > 0 {
|
|
template.PermittedDNSDomainsCritical = true
|
|
template.PermittedDNSDomains = opts.PermittedDNSDomains
|
|
}
|
|
bs, err := x509.CreateCertificate(
|
|
rand.Reader, &template, &template, signer.Public(), signer)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("error generating CA certificate: %s", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("error encoding private key: %s", err)
|
|
}
|
|
|
|
return buf.String(), pk, nil
|
|
}
|
|
|
|
// GenerateCert generates a new certificate for agent TLS (not to be confused with Connect TLS)
|
|
func GenerateCert(opts CertOpts) (string, string, error) {
|
|
parent, err := parseCert(opts.CA)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("failed to parse CA: %w", err)
|
|
}
|
|
|
|
signee, pk, err := GeneratePrivateKey()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("failed to generate private key: %w", err)
|
|
}
|
|
|
|
id, err := keyID(signee.Public())
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("failed to get keyID from public key: %w", err)
|
|
}
|
|
|
|
sn := opts.Serial
|
|
if sn == nil {
|
|
var err error
|
|
sn, err = GenerateSerialNumber()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
template := x509.Certificate{
|
|
SerialNumber: sn,
|
|
Subject: pkix.Name{CommonName: opts.Name},
|
|
BasicConstraintsValid: true,
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
ExtKeyUsage: opts.ExtKeyUsage,
|
|
IsCA: false,
|
|
NotAfter: time.Now().AddDate(0, 0, opts.Days),
|
|
NotBefore: time.Now(),
|
|
SubjectKeyId: id,
|
|
DNSNames: opts.DNSNames,
|
|
IPAddresses: opts.IPAddresses,
|
|
}
|
|
if opts.IsCA {
|
|
template.IsCA = true
|
|
template.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature
|
|
}
|
|
|
|
bs, err := x509.CreateCertificate(rand.Reader, &template, parent, signee.Public(), opts.Signer)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("failed to create certificate: %w", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("error encoding private key: %s", err)
|
|
}
|
|
|
|
return buf.String(), pk, nil
|
|
}
|
|
|
|
// KeyId returns a x509 KeyId from the given signing key.
|
|
func keyID(raw interface{}) ([]byte, error) {
|
|
switch raw.(type) {
|
|
case *ecdsa.PublicKey:
|
|
case *rsa.PublicKey:
|
|
default:
|
|
return nil, fmt.Errorf("invalid key type: %T", raw)
|
|
}
|
|
|
|
// This is not standard; RFC allows any unique identifier as long as they
|
|
// match in subject/authority chains but suggests specific hashing of DER
|
|
// bytes of public key including DER tags.
|
|
bs, err := x509.MarshalPKIXPublicKey(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// String formatted
|
|
kID := sha256.Sum256(bs)
|
|
return kID[:], nil
|
|
}
|
|
|
|
func parseCert(pemValue string) (*x509.Certificate, error) {
|
|
// The _ result below is not an error but the remaining PEM bytes.
|
|
block, _ := pem.Decode([]byte(pemValue))
|
|
if block == nil {
|
|
return nil, fmt.Errorf("no PEM-encoded data found")
|
|
}
|
|
|
|
if block.Type != "CERTIFICATE" {
|
|
return nil, fmt.Errorf("first PEM-block should be CERTIFICATE type")
|
|
}
|
|
|
|
return x509.ParseCertificate(block.Bytes)
|
|
}
|
|
|
|
// ParseSigner parses a crypto.Signer from a PEM-encoded key. The private key
|
|
// is expected to be the first block in the PEM value.
|
|
func ParseSigner(pemValue string) (crypto.Signer, error) {
|
|
return connect.ParseSigner(pemValue)
|
|
}
|
|
|
|
func Verify(caString, certString, dns string) error {
|
|
roots := x509.NewCertPool()
|
|
ok := roots.AppendCertsFromPEM([]byte(caString))
|
|
if !ok {
|
|
return fmt.Errorf("failed to parse root certificate")
|
|
}
|
|
|
|
cert, err := parseCert(certString)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse certificate")
|
|
}
|
|
|
|
opts := x509.VerifyOptions{
|
|
DNSName: fmt.Sprint(dns),
|
|
Roots: roots,
|
|
}
|
|
|
|
_, err = cert.Verify(opts)
|
|
return err
|
|
}
|