Skip to content

Commit 7d178d8

Browse files
committed
add support for RSA private keys
This commit adds support for RSA private keys. See the `mtls.RSAPrivateKey` struct. RSA keys use the `k3:` prefix and are significantly larger than EdDSA / ECDSA keys. Currently, their text representation includes the private key parameter `D` which is not strictly required since it can be re-computed using the public exponent `E`. However, due to FIPS 140, `D` would have to be computed not using the typical φ(N) where φ(N) = (p-1)(q-1) but using E⁻¹ mod λ(N) where λ(N) = lcm(p-1, q-1). This commit also adds an RSA certificate used for tests generated with OpenSSL 3.6.0 Signed-off-by: Andreas Auernhammer <github@aead.dev>
1 parent d832997 commit 7d178d8

File tree

3 files changed

+287
-24
lines changed

3 files changed

+287
-24
lines changed

key.go

Lines changed: 237 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,21 @@ import (
1212
"crypto/ed25519"
1313
"crypto/elliptic"
1414
"crypto/rand"
15+
"crypto/rsa"
1516
"crypto/sha256"
1617
"crypto/tls"
1718
"crypto/x509"
1819
"crypto/x509/pkix"
1920
"encoding/asn1"
2021
"encoding/base64"
22+
"encoding/binary"
2123
"encoding/pem"
2224
"errors"
25+
"fmt"
2326
"io"
27+
"math"
2428
"math/big"
2529
"strconv"
26-
"strings"
2730
"time"
2831
)
2932

@@ -45,27 +48,34 @@ type PrivateKey interface {
4548

4649
// ParsePrivateKey parses s and returns it as PrivateKey.
4750
//
48-
// Currently, ParsePrivateKey either returns a *EdDSAPrivateKey,
49-
// a *ECDSAPrivateKey or an error.
51+
// Currently, ParsePrivateKey either returns a [*EdDSAPrivateKey],
52+
// a [*ECDSAPrivateKey], a [*RSAPrivateKey] or an error.
5053
func ParsePrivateKey(s string) (PrivateKey, error) {
51-
switch {
52-
default:
53-
return nil, errors.New("mtls: invalid private key")
54-
55-
case strings.HasPrefix(s, "k1:"):
56-
var key EdDSAPrivateKey
57-
if err := key.UnmarshalText([]byte(s)); err != nil {
58-
return nil, err
54+
if len(s) >= 3 {
55+
switch s[:3] {
56+
case "k1:":
57+
var key EdDSAPrivateKey
58+
if err := key.UnmarshalText([]byte(s)); err != nil {
59+
return nil, err
60+
}
61+
return &key, nil
62+
63+
case "k2:":
64+
var key ECDSAPrivateKey
65+
if err := key.UnmarshalText([]byte(s)); err != nil {
66+
return nil, err
67+
}
68+
return &key, nil
69+
70+
case "k3:":
71+
var key RSAPrivateKey
72+
if err := key.UnmarshalText([]byte(s)); err != nil {
73+
return nil, err
74+
}
75+
return &key, nil
5976
}
60-
return &key, nil
61-
62-
case strings.HasPrefix(s, "k2:"):
63-
var key ECDSAPrivateKey
64-
if err := key.UnmarshalText([]byte(s)); err != nil {
65-
return nil, err
66-
}
67-
return &key, nil
6877
}
78+
return nil, errors.New("mtls: invalid private key")
6979
}
7080

7181
// EdDSAPrivateKey is a [PrivateKey] for the EdDSA signature algorithm
@@ -210,7 +220,7 @@ func (pk *ECDSAPrivateKey) Private() crypto.PrivateKey {
210220
}
211221
}
212222

213-
// Private returns the ECDSA public key.
223+
// Public returns the ECDSA public key.
214224
func (pk *ECDSAPrivateKey) Public() crypto.PublicKey {
215225
var X, Y big.Int
216226
return &ecdsa.PublicKey{
@@ -223,9 +233,9 @@ func (pk *ECDSAPrivateKey) Public() crypto.PublicKey {
223233
// Identity returns the identity of the ECDSA public key.
224234
func (pk *ECDSAPrivateKey) Identity() Identity { return pk.identity }
225235

226-
// MarshalText returns a textual representation of the private key.
236+
// MarshalText returns a textual representation of the ECDSA private key.
227237
//
228-
// It returns output equivalent to [ECDSAPrivateKey.String]
238+
// It returns output equivalent to [ECDSAPrivateKey.String].
229239
func (pk *ECDSAPrivateKey) MarshalText() ([]byte, error) {
230240
// We use FillBytes instead of Bytes since the later returns
231241
// a variable-size slice. However, we want all private key
@@ -241,7 +251,7 @@ func (pk *ECDSAPrivateKey) MarshalText() ([]byte, error) {
241251
return b, nil
242252
}
243253

244-
// UnmarshalText parses an private key textual representation.
254+
// UnmarshalText parses a ECDSA private key textual representation.
245255
func (pk *ECDSAPrivateKey) UnmarshalText(text []byte) error {
246256
if !bytes.HasPrefix(text, []byte("k2:")) {
247257
return errors.New("mtls: invalid ECDSA private key")
@@ -313,9 +323,166 @@ func (pk *ECDSAPrivateKey) String() string {
313323
return "k2:" + base64.RawURLEncoding.EncodeToString(priv)
314324
}
315325

326+
// GenerateKey generates a random RSA private key of the given bit size.
327+
//
328+
// If bits is less than 1024, [GenerateKeyRSA] returns an error. See the
329+
// "[Minimum key size]" section for further details.
330+
//
331+
// Most applications should use [crypto/rand.Reader] as random. Note that the
332+
// returned key does not depend deterministically on the bytes read from rand,
333+
// and may change between calls and/or between versions.
334+
//
335+
// [Minimum key size]: https://pkg.go.dev/crypto/rsa#hdr-Minimum_key_size
336+
func GenerateKeyRSA(random io.Reader, bits int) (*RSAPrivateKey, error) {
337+
priv, err := rsa.GenerateKey(random, bits)
338+
if err != nil {
339+
return nil, err
340+
}
341+
if priv.E > math.MaxUint32 {
342+
return nil, errors.New("mtls: public RSA exponent " + strconv.Itoa(priv.E) + " is too large")
343+
}
344+
priv.Precompute()
345+
346+
identity, err := rsaIdentity(priv)
347+
if err != nil {
348+
return nil, err
349+
}
350+
return &RSAPrivateKey{
351+
priv: priv,
352+
identity: identity,
353+
}, nil
354+
}
355+
356+
// RSAPrivateKey represents an RSA [PrivateKey].
357+
type RSAPrivateKey struct {
358+
priv *rsa.PrivateKey
359+
identity Identity
360+
}
361+
362+
// Private returns the RSA private key.
363+
func (pk *RSAPrivateKey) Private() crypto.PrivateKey { return pk.priv }
364+
365+
// Public returns the RSA public key.
366+
func (pk *RSAPrivateKey) Public() crypto.PublicKey { return pk.priv.Public() }
367+
368+
// Identity returns the identity of the RSA public key.
369+
func (pk *RSAPrivateKey) Identity() Identity { return pk.identity }
370+
371+
// MarshalText returns a textual representation of the private key.
372+
//
373+
// It returns output equivalent to [RSAPrivateKey.String].
374+
func (pk *RSAPrivateKey) MarshalText() ([]byte, error) {
375+
return base64.RawURLEncoding.AppendEncode([]byte("k3:"), pk.encode()), nil
376+
}
377+
378+
// UnmarshalText parses a textual representation of an RSA private key.
379+
func (pk *RSAPrivateKey) UnmarshalText(text []byte) error {
380+
if !bytes.HasPrefix(text, []byte("k3:")) {
381+
return errors.New("mtls: invalid RSA private key")
382+
}
383+
text = text[3:]
384+
385+
var err error
386+
data := make([]byte, 0, base64.RawURLEncoding.DecodedLen(len(text)))
387+
if data, err = base64.RawURLEncoding.AppendDecode(data, text); err != nil {
388+
return err
389+
}
390+
391+
var E uint32
392+
if len(data) < 6 {
393+
return errors.New("mtls: invalid RSA private key parameter E")
394+
}
395+
if n := binary.BigEndian.Uint16(data); n != 4 {
396+
return errors.New("mtls: invalid RSA private key parameter E: invalid encoding length " + strconv.Itoa(int(n)))
397+
}
398+
E = binary.BigEndian.Uint32(data[2:])
399+
data = data[6:]
400+
401+
var P, Q, D *big.Int
402+
if data, P, err = decodeRSAParam(data); err != nil {
403+
return fmt.Errorf("mtls: invalid RSA private key parameter P: %w", err)
404+
}
405+
if data, Q, err = decodeRSAParam(data); err != nil {
406+
return fmt.Errorf("mtls: invalid RSA private key parameter Q: %w", err)
407+
}
408+
if data, D, err = decodeRSAParam(data); err != nil {
409+
return fmt.Errorf("mtls: invalid RSA private key parameter D: %w", err)
410+
}
411+
if len(data) != 0 {
412+
return errors.New("mtls: invalid RSA private key: private key contains additional data")
413+
}
414+
415+
priv := &rsa.PrivateKey{
416+
PublicKey: rsa.PublicKey{
417+
N: new(big.Int).Mul(P, Q),
418+
E: int(E),
419+
},
420+
D: D,
421+
Primes: []*big.Int{P, Q},
422+
}
423+
priv.Precompute()
424+
if err = priv.Validate(); err != nil {
425+
return fmt.Errorf("mtls: invalid RSA private key: %w", err)
426+
}
427+
428+
identity, err := rsaIdentity(priv)
429+
if err != nil {
430+
return err
431+
}
432+
433+
pk.priv = priv
434+
pk.identity = identity
435+
return nil
436+
}
437+
438+
// String returns a string representation of the private key.
439+
//
440+
// Its output is equivalent to [RSAPrivateKey.MarshalText]
441+
func (pk *RSAPrivateKey) String() string {
442+
return "k3:" + base64.RawURLEncoding.EncodeToString(pk.encode())
443+
}
444+
445+
// encode returns the RSA key's binary representation:
446+
//
447+
// len(E) | E | len(P) | P | len(Q) | Q | len(D) | D
448+
//
449+
// All numbers are represented in big endian.
450+
//
451+
// Values that can be re-computed are omitted to be space efficient.
452+
// For example, the modulus N = PQ. The private exponent D can also
453+
// be re-computed given P and Q as D = E⁻¹ mod φ(N) where φ(N) = (p-1)(q-1).
454+
//
455+
// However, FIPS 186-5 requires computing it as E⁻¹ mod λ(N) where λ(N) = lcm(p-1, q-1).
456+
// Hence, we include D to avoid re-implementing private exponent calculations.
457+
// The binary representation puts D at the end such that we can support shorter
458+
// private keys (without D) in the future.
459+
func (pk *RSAPrivateKey) encode() []byte {
460+
var (
461+
D, P, Q = pk.priv.D, pk.priv.Primes[0], pk.priv.Primes[1]
462+
d, p, q = (D.BitLen() + 7) / 8, (P.BitLen() + 7) / 8, (Q.BitLen() + 7) / 8
463+
464+
buf = make([]byte, max(d, p, q))
465+
out = make([]byte, 0, 12+d+p+q) // length-prefixed encoded len: 2 + 4 + 2 + d + 2 + p + 2 + q
466+
)
467+
468+
out = binary.BigEndian.AppendUint16(out, 4)
469+
out = binary.BigEndian.AppendUint32(out, uint32(pk.priv.E))
470+
471+
out = binary.BigEndian.AppendUint16(out, uint16(p))
472+
out = append(out, P.FillBytes(buf[:p])...)
473+
474+
out = binary.BigEndian.AppendUint16(out, uint16(q))
475+
out = append(out, Q.FillBytes(buf[:q])...)
476+
477+
out = binary.BigEndian.AppendUint16(out, uint16(d))
478+
out = append(out, D.FillBytes(buf[:d])...)
479+
return out
480+
}
481+
316482
var (
317483
oidPublicKeyEdDSA = asn1.ObjectIdentifier{1, 3, 101, 112}
318484
oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
485+
oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
319486

320487
oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7}
321488
oidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34}
@@ -384,7 +551,53 @@ func ecdsaIdentity(key *ecdsa.PrivateKey) (Identity, error) {
384551
}, nil
385552
}
386553

387-
// NewCertificate returns a new TLS certificate using the
554+
func rsaIdentity(key *rsa.PrivateKey) (Identity, error) {
555+
type PKCS1 struct {
556+
N *big.Int
557+
E int
558+
}
559+
pubKey, err := asn1.Marshal(PKCS1{
560+
N: key.PublicKey.N,
561+
E: key.PublicKey.E,
562+
})
563+
if err != nil {
564+
return Identity{}, fmt.Errorf("mtls: failed to encode RSA public key: %w", err)
565+
}
566+
b, err := asn1.Marshal(publicKeyInfo{
567+
Algorithm: pkix.AlgorithmIdentifier{
568+
Algorithm: oidPublicKeyRSA,
569+
Parameters: asn1.NullRawValue,
570+
},
571+
PublicKey: asn1.BitString{BitLength: len(pubKey) * 8, Bytes: pubKey},
572+
})
573+
if err != nil {
574+
return Identity{}, fmt.Errorf("mtls: failed to encode RSA public key: %w", err)
575+
}
576+
577+
return Identity{
578+
hash: sha256.Sum256(b),
579+
}, nil
580+
}
581+
582+
// decodeRSAParam decodes a length-encoded big endian binary
583+
// representation of an RSA private key parameter. In particular,
584+
// the private exponent D and the prime factors P and Q.
585+
func decodeRSAParam(b []byte) ([]byte, *big.Int, error) {
586+
if len(b) < 2 {
587+
return nil, nil, errors.New("invalid length encoding")
588+
}
589+
590+
n := binary.BigEndian.Uint16(b)
591+
if n == 0 {
592+
return nil, nil, errors.New("parameter length is zero")
593+
}
594+
if int(n) > len(b)-2 {
595+
return nil, nil, errors.New("parameter length " + strconv.Itoa(int(n)) + " exceeds data")
596+
}
597+
return b[2+n:], new(big.Int).SetBytes(b[2 : 2+n]), nil
598+
}
599+
600+
// newCertificate returns a new TLS certificate using the
388601
// given private key.
389602
func newCertificate(key PrivateKey) (*tls.Certificate, error) {
390603
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)

key_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"crypto/ed25519"
1010
"crypto/elliptic"
1111
"crypto/rand"
12+
"crypto/rsa"
1213
"crypto/x509"
1314
"encoding/pem"
1415
"os"
@@ -65,9 +66,35 @@ func TestGenerateKeyECDSA(t *testing.T) {
6566
}
6667
}
6768

69+
// TestGenerateKeyRSA tests whether generated RSA private keys
70+
// are equal to their parsed textual representation for common
71+
// key sizes.
72+
func TestGenerateKeyRSA(t *testing.T) {
73+
t.Parallel()
74+
75+
bitSizes := []int{2048, 3072, 4096}
76+
for _, bits := range bitSizes {
77+
key, err := mtls.GenerateKeyRSA(rand.Reader, bits)
78+
if err != nil {
79+
t.Fatalf("failed to generate %d RSA private key: %v", bits, err)
80+
}
81+
82+
s := key.String()
83+
key2, err := mtls.ParsePrivateKey(s)
84+
if err != nil {
85+
t.Fatalf("failed to unmarshal %d RSA private key %s: %v", bits, s, err)
86+
}
87+
if k := key.Private().(*rsa.PrivateKey); !k.Equal(key2.Private()) {
88+
t.Fatalf("private keys are not equal: %s != %s", key, key2)
89+
}
90+
}
91+
}
92+
6893
// TestPrivateKey_Identity checks that a certificate's public key identity of matches the
6994
// identity of the corresponding private key.
7095
func TestPrivateKey_Identity(t *testing.T) {
96+
t.Parallel()
97+
7198
for _, test := range privateKeyIdentityTests {
7299
key, err := mtls.ParsePrivateKey(test.PrivateKey)
73100
if err != nil {
@@ -116,4 +143,8 @@ var privateKeyIdentityTests = []struct {
116143
Filename: "./testdata/certs/p-521.crt",
117144
PrivateKey: "k2:AT7JYw3tnjgYhqplUPiJbITqAdgo4IuDf9talnHivzMeoEsVR60Vidpl93zAdweZApsStCEpHVPtwGAD2UoGI0o0",
118145
},
146+
{
147+
Filename: "./testdata/certs/rsa.crt",
148+
PrivateKey: "k3:AAQAAQABAIDIgYr4PXOsMUpeV-tjoVEs5h6iCudZAkbWA8o3m9C40tIc3ZKPxANNrNDQBv_aQxJsbEvZ8CWgRKuSP3kcFZq1f96Zra99PBvrypAlhtwzehmDUIDJQwlshNIozBRTL_zHFZDKIjMSwiag3bWQmm_ivuNfL-WWIkUpXNU1NvEsWwCAtkeD1ZsCAF1t-_wPl-iHspI-qLz43B1Op2Z7ja-aIYvViYig7a7e_yyr_RR3PePpOiQMcz4HMtWkf3BbyGwSZWm4wlIcNBrLP0oEpDx1sIh1D7yg9aquDiyYuQwFjZwTMP0Vy4KSoz3U1USM-EEICTyM8qBsBp8mGUkOl26ZS2kBAERYqSbToPba3HeNFITdv9_9PLoUsFYl3wKFLNdDDNredfGpkmgeyGl4BLOJ_BR2tNDWbzspLJ45slAe9e-FB5VIgk7lwd_eHBuNSODbUpTkNmge6ZEunQa3j5Yuvh9LxBg-Gwbnm3kSdt8Q0YqrZTwSZMiogAN6aqI1GwjNRHx_8M9718mSGAmlYWHAfXsgxVr3VTMtZjYmGB2_Slu46M4k58Mf2QrOgP9iGz6ED7q2CL3_PIAT1Ce0qKf3UP8IgVm6ENJx5obKdq2UeyV3OQx5HL99_X74f6KjjVtd1XXjrxoSqBP_HZYce8K1FgWjpFMshsPTAHGrsmk_dseDfSk",
149+
},
119150
}

0 commit comments

Comments
 (0)