Skip to content

Commit

Permalink
replace custom API keys with mtls package
Browse files Browse the repository at this point in the history
This commit replaces the custom API keys with the `aead.dev/mtls`
package. It provides an enhanced mTLS implementation and supports
more private key types.

Signed-off-by: Andreas Auernhammer <[email protected]>
  • Loading branch information
aead committed Sep 11, 2024
1 parent 6af6613 commit d50af33
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 238 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.22.5
go-version: 1.22.7
check-latest: true
- name: Check out code
uses: actions/checkout@v3
Expand All @@ -40,7 +40,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.22.5
go-version: 1.22.7
check-latest: true
- name: Check out code
uses: actions/checkout@v3
Expand All @@ -57,7 +57,7 @@ jobs:
uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.22.5
go-version: 1.22.7
check-latest: true
- name: Get govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
Expand Down
2 changes: 1 addition & 1 deletion go.work
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
go 1.21
go 1.22.0

use (
./kes
Expand Down
2 changes: 1 addition & 1 deletion kes/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/minio/kms-go/kes

go 1.21
go 1.22.0

require (
aead.dev/mem v0.2.0
Expand Down
19 changes: 10 additions & 9 deletions kms/client-examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import (
"log/slog"
"time"

"aead.dev/mtls"
"github.com/minio/kms-go/kms"
)

func ExampleNewClient() {
key, err := kms.ParseAPIKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
key, err := mtls.ParsePrivateKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
if err != nil {
log.Fatalf("Failed to parse KMS API key: %v", err)
}
Expand Down Expand Up @@ -55,7 +56,7 @@ func ExampleNewClient() {
// KMS cluster dynamically expanding it. The added KMS server must not
// be part of an exisiting cluster.
func ExampleClient_AddNode() {
key, err := kms.ParseAPIKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
key, err := mtls.ParsePrivateKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
if err != nil {
log.Fatalf("Failed to parse KMS API key: %v", err)
}
Expand Down Expand Up @@ -85,7 +86,7 @@ func ExampleClient_AddNode() {
// ExampleClient_RemoveNode shows how to remove a KMS server from the
// cluster it is currently part of.
func ExampleClient_RemoveNode() {
key, err := kms.ParseAPIKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
key, err := mtls.ParsePrivateKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
if err != nil {
log.Fatalf("Failed to parse KMS API key: %v", err)
}
Expand Down Expand Up @@ -115,7 +116,7 @@ func ExampleClient_RemoveNode() {
// ExampleClient_ClusterStatus shows how to fetch cluster status information
// from a KMS cluster.
func ExampleClient_ClusterStatus() {
key, err := kms.ParseAPIKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
key, err := mtls.ParsePrivateKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
if err != nil {
log.Fatalf("Failed to parse KMS API key: %v", err)
}
Expand Down Expand Up @@ -143,7 +144,7 @@ func ExampleClient_ClusterStatus() {

// ExampleClient_CreateEnclave shows how to create a new enclave.
func ExampleClient_CreateEnclave() {
key, err := kms.ParseAPIKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
key, err := mtls.ParsePrivateKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
if err != nil {
log.Fatalf("Failed to parse KMS API key: %v", err)
}
Expand Down Expand Up @@ -172,7 +173,7 @@ func ExampleClient_CreateEnclave() {

// ExampleClient_DeleteEnclave shows how to delete an existing enclave.
func ExampleClient_DeleteEnclave() {
key, err := kms.ParseAPIKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
key, err := mtls.ParsePrivateKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
if err != nil {
log.Fatalf("Failed to parse KMS API key: %v", err)
}
Expand Down Expand Up @@ -202,7 +203,7 @@ func ExampleClient_DeleteEnclave() {
// ExampleClient_EnclaveStatus shows how to fetch status information about two enclaves.
// Fetching information about multiple enclaves requires just a single network request.
func ExampleClient_EnclaveStatus() {
key, err := kms.ParseAPIKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
key, err := mtls.ParsePrivateKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
if err != nil {
log.Fatalf("Failed to parse KMS API key: %v", err)
}
Expand Down Expand Up @@ -238,7 +239,7 @@ func ExampleClient_EnclaveStatus() {
// ExampleClient_EnclaveStatus shows how to fetch status information about two enclaves.
// Fetching information about multiple enclaves requires just a single network request.
func ExampleClient_ListEnclaves() {
key, err := kms.ParseAPIKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
key, err := mtls.ParsePrivateKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
if err != nil {
log.Fatalf("Failed to parse KMS API key: %v", err)
}
Expand Down Expand Up @@ -270,7 +271,7 @@ func ExampleClient_ListEnclaves() {

// ExampleClient_Logs shows how to fetch server log records.
func ExampleClient_Logs() {
key, err := kms.ParseAPIKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
key, err := mtls.ParsePrivateKey("k1:d7cY_5k8HbBGkZpoy2hGmvkxg83QDBXsA_nFXDfTk2E")
if err != nil {
log.Fatalf("Failed to parse KMS API key: %v", err)
}
Expand Down
3 changes: 2 additions & 1 deletion kms/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"time"

"aead.dev/mem"
"aead.dev/mtls"
"github.com/minio/kms-go/kms/cmds"
"github.com/minio/kms-go/kms/internal/api"
"github.com/minio/kms-go/kms/internal/headers"
Expand All @@ -42,7 +43,7 @@ type Config struct {
//
// When providing an API key, no TLS.Certificates
// or TLS.GetClientCertificate must be present.
APIKey APIKey
APIKey mtls.PrivateKey

// Optional TLS configuration.
//
Expand Down
32 changes: 0 additions & 32 deletions kms/example_test.go

This file was deleted.

3 changes: 2 additions & 1 deletion kms/go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
module github.com/minio/kms-go/kms

go 1.21
go 1.22.0

require (
aead.dev/mem v0.2.0
aead.dev/mtls v0.1.0
google.golang.org/protobuf v1.33.0
)
2 changes: 2 additions & 0 deletions kms/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
aead.dev/mem v0.2.0 h1:ufgkESS9+lHV/GUjxgc2ObF43FLZGSemh+W+y27QFMI=
aead.dev/mem v0.2.0/go.mod h1:4qj+sh8fjDhlvne9gm/ZaMRIX9EkmDrKOLwmyDtoMWM=
aead.dev/mtls v0.1.0 h1:zT/p8BpBXol0bmRaJz4WyMW3wTjSjJztKlSoXl/a+rs=
aead.dev/mtls v0.1.0/go.mod h1:QirwQiq697/G62Ug6LbzfeR4Nq1vBGch5ajUZdAzJaw=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
Expand Down
154 changes: 3 additions & 151 deletions kms/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,18 @@
package kms

import (
"crypto"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"errors"
"io"
"math/big"
"strconv"
"strings"
"time"
)

// An Identity uniquely identifies a private/public key pair.
// It consists of a prefix for the hash function followed by
// the URL base64-encoded hash of the public key.
//
// For example:
//
// h1:BPbFim5DqUozIYOjcaRAtImU6TdD6W2_chOgxDyCuDw
//
// This package uses the "h1:" prefix for SHA-256 and computes
// the hash of X.509 certificates from the certificate's
// DER-encoded public key info.
//
// For example:
//
// shasum := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
// identity := "h1:" + base64.RawURLEncoding.EncodeToString(shasum[:])
//
// By verifying the peer's identity, two parties can detect
// MitM¹ attacks during a protocol handshake, like in TLS.
// An identity pins the public key, similar to SSH² or HPKP³.
//
// The empty string represents a pseudo identity and indicates
// that no public key has been provided.
//
// Ref:
// [1] https://en.wikipedia.org/wiki/Man-in-the-middle_attack
// [2] https://en.wikipedia.org/wiki/Key_fingerprint
// [3] https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning
type Identity string

func (i Identity) String() string { return string(i) }
"aead.dev/mtls"
)

// Privilege represents an access control role of identities.
// An identity with a higher privilege has access to more APIs.
Expand Down Expand Up @@ -111,85 +74,12 @@ func (p Privilege) String() string {
}
}

// An APIKey represents a public/private key pair.
// An API key can be used to authenticate to a TLS server via
// mTLS¹ by generating a X.509 certificate from the API key's
// public key.
//
// Ref:
// [1] https://en.wikipedia.org/wiki/Mutual_authentication#mTLS
type APIKey interface {
// Public returns the API key's public key.
Public() crypto.PublicKey

// Private returns the API key's private key.
Private() crypto.PrivateKey

// Identity returns the Identity associated with the
// public key.
Identity() Identity

// String returns the API key's string representation.
String() string
}

// GenerateAPIKey generates a new API key using the given
// io.Reader as source of randomness.
//
// If random is nil, the standard library crypto/rand.Reader
// is used.
func GenerateAPIKey(random io.Reader) (APIKey, error) {
pub, priv, err := ed25519.GenerateKey(random)
if err != nil {
return nil, err
}

id, err := ed25519Identity(pub)
if err != nil {
return nil, err
}
return &apiKey{
key: priv,
identity: id,
}, nil
}

// ParseAPIKey parses s as formatted API key.
func ParseAPIKey(s string) (APIKey, error) {
s, found := strings.CutPrefix(s, "k1:")
if !found {
return nil, errors.New("kms: invalid API key type")
}

if base64.RawURLEncoding.DecodedLen(len(s)) != ed25519.SeedSize {
return nil, errors.New("kms: invalid API key length")
}

b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return nil, err
}
if len(b) != ed25519.SeedSize {
return nil, errors.New("kms: invalid API key length")
}

key := ed25519.NewKeyFromSeed(b)
id, err := ed25519Identity(key[ed25519.SeedSize:])
if err != nil {
return nil, err
}
return &apiKey{
key: key,
identity: id,
}, nil
}

// GenerateCertificate generates a new self-signed TLS certificate
// from the given template using the APIKey's private and public key.
//
// The template may be nil. In such a case the returned certificate
// is generated using a default template and valid for 90 days.
func GenerateCertificate(key APIKey, template *x509.Certificate) (tls.Certificate, error) {
func GenerateCertificate(key mtls.PrivateKey, template *x509.Certificate) (tls.Certificate, error) {
if template == nil {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
Expand Down Expand Up @@ -232,41 +122,3 @@ func GenerateCertificate(key APIKey, template *x509.Certificate) (tls.Certificat
}
return cert, nil
}

// apiKey is an APIKey implementation using Ed25519 public/private keys.
type apiKey struct {
key ed25519.PrivateKey
identity Identity
}

func (ak *apiKey) Public() crypto.PublicKey { return ak.key.Public() }

func (ak *apiKey) Private() crypto.PrivateKey {
private := make([]byte, 0, len(ak.key))
return ed25519.PrivateKey(append(private, ak.key...))
}

func (ak *apiKey) Identity() Identity { return ak.identity }

func (ak *apiKey) String() string {
return "k1:" + base64.RawURLEncoding.EncodeToString(ak.key[:ed25519.SeedSize])
}

func ed25519Identity(pubKey []byte) (Identity, error) {
type publicKeyInfo struct {
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}

derPublicKey, err := asn1.Marshal(publicKeyInfo{
Algorithm: pkix.AlgorithmIdentifier{
Algorithm: asn1.ObjectIdentifier{1, 3, 101, 112},
},
PublicKey: asn1.BitString{BitLength: len(pubKey) * 8, Bytes: pubKey},
})
if err != nil {
return "", err
}
id := sha256.Sum256(derPublicKey)
return "h1:" + Identity(base64.RawURLEncoding.EncodeToString(id[:])), nil
}
Loading

0 comments on commit d50af33

Please sign in to comment.