Skip to content

Commit

Permalink
replace custom API keys with mtls package (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
aead authored Sep 11, 2024
1 parent 6af6613 commit e82414e
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 e82414e

Please sign in to comment.