From 538ea436ca25dd177a3525551d0e0a47c9cfec16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 8 Jul 2025 12:57:49 +0200 Subject: [PATCH] WIP RSA support --- crypto/rsa/key.go | 30 +++++++ crypto/rsa/key_test.go | 89 ++++++++++++++++++++ crypto/rsa/private.go | 181 +++++++++++++++++++++++++++++++++++++++++ crypto/rsa/public.go | 134 ++++++++++++++++++++++++++++++ 4 files changed, 434 insertions(+) create mode 100644 crypto/rsa/key.go create mode 100644 crypto/rsa/key_test.go create mode 100644 crypto/rsa/private.go create mode 100644 crypto/rsa/public.go diff --git a/crypto/rsa/key.go b/crypto/rsa/key.go new file mode 100644 index 0000000..9852285 --- /dev/null +++ b/crypto/rsa/key.go @@ -0,0 +1,30 @@ +package rsa + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" +) + +const ( + MultibaseCode = uint64(0x1205) + + MinRsaKeyBits = 2048 + MaxRsaKeyBits = 8192 +) + +func GenerateKeyPair(bits int) (*PublicKey, *PrivateKey, error) { + if bits < MinRsaKeyBits || bits > MaxRsaKeyBits { + return nil, nil, fmt.Errorf("invalid key size: %d", bits) + } + priv, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + return &PublicKey{k: &priv.PublicKey}, &PrivateKey{k: priv}, nil +} + +const ( + pemPubBlockType = "PUBLIC KEY" + pemPrivBlockType = "PRIVATE KEY" +) diff --git a/crypto/rsa/key_test.go b/crypto/rsa/key_test.go new file mode 100644 index 0000000..48e9e4d --- /dev/null +++ b/crypto/rsa/key_test.go @@ -0,0 +1,89 @@ +package rsa + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/INFURA/go-did/crypto/_testsuite" +) + +var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{ + Name: "rsa-2048", + GenerateKeyPair: func() (*PublicKey, *PrivateKey, error) { return GenerateKeyPair(2048) }, + PublicKeyFromPublicKeyMultibase: PublicKeyFromPublicKeyMultibase, + PublicKeyFromX509DER: PublicKeyFromX509DER, + PublicKeyFromX509PEM: PublicKeyFromX509PEM, + PrivateKeyFromPKCS8DER: PrivateKeyFromPKCS8DER, + PrivateKeyFromPKCS8PEM: PrivateKeyFromPKCS8PEM, + MultibaseCode: MultibaseCode, + SignatureBytesSize: 123456, +} + +func TestSuite(t *testing.T) { + testsuite.TestSuite(t, harness) +} + +func BenchmarkSuite(b *testing.B) { + testsuite.BenchSuite(b, harness) +} + +func TestPublicKeyX509(t *testing.T) { + // openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 + // openssl pkey -in private_key.pem -pubout -out public_key.pem + pem := `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyLFQUbVVo/rctJaCzR5z +g622eUNBwZmA1vnDEXnHWBl3y5RJF5zyTdlouujjmEuu6qsXk1NCNQ3dLH2iquI8 +iFFAhS4kTX6JS+wR3vHLhga1oFkPceGFEUG/3vxn52ozFs8hikhq/P09HmLub7Vc +VklwrGvTbEa5Fn/2Kz6olw5ExYI14Unsl+A3iw8AXPL9/acD+ehoyx3/zKFrVTKx +e9jdoWX8L7IpqM2HOSu23/3E2IwH2GdY0C8575AiD/O555hie7JHkzF3I4E85gPd +ZgXYFShIfgOzDV0q4oP0pzqYkErhdjOpigCMjDuIC4OueZYqYJrP2rdpzuqoqk07 +NwIDAQAB +-----END PUBLIC KEY----- +` + + pub, err := PublicKeyFromX509PEM(pem) + require.NoError(t, err) + + rt := pub.ToX509PEM() + require.Equal(t, pem, rt) +} + +func TestPrivateKeyPKCS8(t *testing.T) { + // openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 + pem := `-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDIsVBRtVWj+ty0 +loLNHnODrbZ5Q0HBmYDW+cMRecdYGXfLlEkXnPJN2Wi66OOYS67qqxeTU0I1Dd0s +faKq4jyIUUCFLiRNfolL7BHe8cuGBrWgWQ9x4YURQb/e/GfnajMWzyGKSGr8/T0e +Yu5vtVxWSXCsa9NsRrkWf/YrPqiXDkTFgjXhSeyX4DeLDwBc8v39pwP56GjLHf/M +oWtVMrF72N2hZfwvsimozYc5K7bf/cTYjAfYZ1jQLznvkCIP87nnmGJ7skeTMXcj +gTzmA91mBdgVKEh+A7MNXSrig/SnOpiQSuF2M6mKAIyMO4gLg655lipgms/at2nO +6qiqTTs3AgMBAAECggEAVFVqZoN4QumSYBKVUYOX0AAp2ygflC6gnPWkeo39bjB5 +jiM4WcNacMtIvq5JoYBANx2BUSfd/PRf+ierOPrLrA7UuYJLwALJyA0h71kVCLN+ +FC0Il/bIF5nU+mt/cBfI8y9ELVtEFh6GVeQFxQxlil7fCZ1f4TKQ6XsJI1/3sU2P +hbOuyfKKiWym8n5BV6NP3gotjnT01I+seplx3oMOKIaGl0KMgkuU2r8o8WMjA7Gx +1WWPJDpUdyYDYSUH8PubXowHkE+2RXddZ+tGvS8mF/A4Q0hdj2T9XvzyZ813O9Tv +n522A9QQE8YlqwAYh4z3VoNhz+Fi1mQfYsIblNygSQKBgQDrk+kB/dz92RPhP/rh +zAOvwRuI2TOaw98kdgpVlb6gMVmN2EWkzkdnwQDJhV+MFZob4wi+TpsDPv4fjubq +gqbM/MYc0kNtIEA4GkIJLCK5Hh7c6kCQfya+/eq4Ju6C3+I4R46/+9E7ixA83Zjf +ftqTlYOrlMby84Lvsf81LtiMiQKBgQDaFzXpDBPOIaup68k9NeZyXHKI8wNQXkui +JyjM9A3U2D8O9Yty8G+Oq0B4oUGlyenMGJiQmf3bAffJBkLCMXCGXYD8CCKsiSJ6 +R6XBfbpPkzCwl67FFN/8Z0nxZ0lbxd2ZMTC4qxH4peD5TNZM89kTpSNXPrr55zzm +qREmxisZvwKBgQCNK3jBScjpkfFY1UdZkjFPXDBM5KQJBYGtztLIkNDIHGqnFsg9 +R6QAp+b53GPyhWtxdK7jpCU+X7xXWwJD3AFq67sowFPJjD8Pn6Sc7IbuWf9ysSn5 +rUihwXWr3yCk6tcclL0VjSjIPsB/SOf4XoNLV5is9J34Lzbyvr7JtwXryQKBgQCM +m3xRdUzrkD/J/M+w3ChoQPxDGVJgpXrj35Vplku4l3cIYPz4LNXvyK93VpgpmGVZ +Bd6PFAlcAwfLHnM6Gn/u0SgQ1fns/TkyVzEh77qIBWDV6eVvAQdsBvfgYPQl7Arz +8ofz969NfTzv3j8oO+sPxF9lp3cLGa/lEsmREyDEpwKBgQCvW+NK93oajo358gKh +/xfSv7yMiSL26NcIgHmQouZVXJ3Dg0KSISx8tgY0/7TwC2mPa0Ryhpb/3HtAIXoY +eqkQGHqnC4voxSoati667mMGdHL1+12WvQmhfTLCWmZ5ccNlR+aFD20TGbMxnejS +XnARctVkIcUYORcYwvuu9meDkw== +-----END PRIVATE KEY----- +` + + priv, err := PrivateKeyFromPKCS8PEM(pem) + require.NoError(t, err) + + rt := priv.ToPKCS8PEM() + require.Equal(t, pem, rt) +} diff --git a/crypto/rsa/private.go b/crypto/rsa/private.go new file mode 100644 index 0000000..34e800e --- /dev/null +++ b/crypto/rsa/private.go @@ -0,0 +1,181 @@ +package rsa + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" + + "github.com/INFURA/go-did/crypto" +) + +var _ crypto.PrivateKeySigning = &PrivateKey{} + +type PrivateKey struct { + k *rsa.PrivateKey +} + +func PrivateKeyFromNEDPQ(n, e, d, p, q []byte) (*PrivateKey, error) { + pub, err := PublicKeyFromNE(n, e) + if err != nil { + return nil, err + } + dBInt := new(big.Int).SetBytes(d) + pBInt := new(big.Int).SetBytes(p) + qBInt := new(big.Int).SetBytes(q) + + priv := &rsa.PrivateKey{ + PublicKey: *pub.k, + D: dBInt, + Primes: []*big.Int{pBInt, qBInt}, + } + + // // while go doesn't care, we ensure to have the JWK canonical order of primes, + // // so that the JWK code becomes simpler + // if subtle.ConstantTimeCompare(p, q) > 0 { + // priv.Primes[0], priv.Primes[1] = priv.Primes[1], priv.Primes[0] + // } + + err = priv.Validate() + if err != nil { + return nil, err + } + priv.Precompute() + + return &PrivateKey{k: priv}, nil +} + +// PrivateKeyFromPKCS8DER decodes a PKCS#8 DER (binary) encoded private key. +func PrivateKeyFromPKCS8DER(bytes []byte) (*PrivateKey, error) { + priv, err := x509.ParsePKCS8PrivateKey(bytes) + if err != nil { + return nil, err + } + rsaPriv := priv.(*rsa.PrivateKey) + return &PrivateKey{k: rsaPriv}, nil +} + +// PrivateKeyFromPKCS8PEM decodes an PKCS#8 PEM (string) encoded private key. +func PrivateKeyFromPKCS8PEM(str string) (*PrivateKey, error) { + block, _ := pem.Decode([]byte(str)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + if block.Type != pemPrivBlockType { + return nil, fmt.Errorf("incorrect PEM block type") + } + return PrivateKeyFromPKCS8DER(block.Bytes) +} + +func (p *PrivateKey) BitLen() int { + return p.k.N.BitLen() +} + +func (p *PrivateKey) DBytes() []byte { + byteLength := (p.k.D.BitLen() + 7) / 8 // Round up to the nearest byte + buf := make([]byte, byteLength) + p.k.D.FillBytes(buf) + return buf +} + +func (p *PrivateKey) PBytes() []byte { + byteLength := (p.k.Primes[0].BitLen() + 7) / 8 // Round up to the nearest byte + buf := make([]byte, byteLength) + p.k.Primes[0].FillBytes(buf) + return buf +} + +func (p *PrivateKey) QBytes() []byte { + byteLength := (p.k.Primes[1].BitLen() + 7) / 8 // Round up to the nearest byte + buf := make([]byte, byteLength) + p.k.Primes[1].FillBytes(buf) + return buf +} + +func (p *PrivateKey) DpBytes() []byte { + if p.k.Precomputed.Dp == nil { + p.k.Precompute() + } + byteLength := (p.k.Precomputed.Dp.BitLen() + 7) / 8 // Round up to the nearest byte + buf := make([]byte, byteLength) + p.k.Precomputed.Dp.FillBytes(buf) + return buf +} + +func (p *PrivateKey) DqBytes() []byte { + if p.k.Precomputed.Dq == nil { + p.k.Precompute() + } + byteLength := (p.k.Precomputed.Dq.BitLen() + 7) / 8 // Round up to the nearest byte + buf := make([]byte, byteLength) + p.k.Precomputed.Dq.FillBytes(buf) + return buf +} + +func (p *PrivateKey) QiBytes() []byte { + if p.k.Precomputed.Qinv == nil { + p.k.Precompute() + } + byteLength := (p.k.Precomputed.Qinv.BitLen() + 7) / 8 // Round up to the nearest byte + buf := make([]byte, byteLength) + p.k.Precomputed.Qinv.FillBytes(buf) + return buf +} + +func (p *PrivateKey) Equal(other crypto.PrivateKey) bool { + if other, ok := other.(*PrivateKey); ok { + return p.k.Equal(other.k) + } + return false +} + +func (p *PrivateKey) Public() crypto.PublicKey { + rsaPub := p.k.Public().(*rsa.PublicKey) + return &PublicKey{k: rsaPub} +} + +func (p *PrivateKey) ToPKCS8DER() []byte { + res, _ := x509.MarshalPKCS8PrivateKey(p.k) + return res +} + +func (p *PrivateKey) ToPKCS8PEM() string { + der := p.ToPKCS8DER() + return string(pem.EncodeToMemory(&pem.Block{ + Type: pemPrivBlockType, + Bytes: der, + })) +} + +func (p *PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) { + return nil, fmt.Errorf("not implemented") +} + +func (p *PrivateKey) SignToASN1(message []byte, opts ...crypto.SigningOption) ([]byte, error) { + return nil, fmt.Errorf("not implemented") +} + +// func (p *PrivateKey) PublicKeyIsCompatible(remote crypto.PublicKey) bool { +// if _, ok := remote.(*PublicKey); ok { +// return true +// } +// return false +// } +// +// func (p *PrivateKey) KeyExchange(remote crypto.PublicKey) ([]byte, error) { +// if remote, ok := remote.(*PublicKey); ok { +// // First, we need to convert the ECDSA (signing only) to the equivalent ECDH keys +// ecdhPriv, err := p.k.ECDH() +// if err != nil { +// return nil, err +// } +// ecdhPub, err := remote.k.ECDH() +// if err != nil { +// return nil, err +// } +// +// return ecdhPriv.ECDH(ecdhPub) +// } +// return nil, fmt.Errorf("incompatible public key") +// } diff --git a/crypto/rsa/public.go b/crypto/rsa/public.go new file mode 100644 index 0000000..edee2c7 --- /dev/null +++ b/crypto/rsa/public.go @@ -0,0 +1,134 @@ +package rsa + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" + + "github.com/INFURA/go-did/crypto" + helpers "github.com/INFURA/go-did/crypto/internal" +) + +var _ crypto.PublicKeySigning = &PublicKey{} + +type PublicKey struct { + k *rsa.PublicKey +} + +func PublicKeyFromPKCS1DER(bytes []byte) (*PublicKey, error) { + pub, err := x509.ParsePKCS1PublicKey(bytes) + if err != nil { + return nil, err + } + return &PublicKey{k: pub}, nil +} + +func PublicKeyFromNE(n, e []byte) (*PublicKey, error) { + nBInt := new(big.Int).SetBytes(n) + // some basic checks + if nBInt.Sign() <= 0 { + return nil, fmt.Errorf("invalid modulus") + } + if nBInt.BitLen() < MinRsaKeyBits { + return nil, fmt.Errorf("key length too small") + } + if nBInt.BitLen() > MaxRsaKeyBits { + return nil, fmt.Errorf("key length too large") + } + if nBInt.Bit(0) == 0 { + return nil, fmt.Errorf("modulus must be odd") + } + + eBInt := new(big.Int).SetBytes(e) + // some basic checks + if !eBInt.IsInt64() { + return nil, fmt.Errorf("invalid exponent") + } + if eBInt.Sign() <= 0 { + return nil, fmt.Errorf("exponent must be positive") + } + if eBInt.Bit(0) == 0 { + return nil, fmt.Errorf("exponent must be odd") + } + return &PublicKey{k: &rsa.PublicKey{N: nBInt, E: int(eBInt.Int64())}}, nil +} + +// PublicKeyFromPublicKeyMultibase decodes the public key from its Multibase form +func PublicKeyFromPublicKeyMultibase(multibase string) (*PublicKey, error) { + code, bytes, err := helpers.PublicKeyMultibaseDecode(multibase) + if err != nil { + return nil, err + } + if code != MultibaseCode { + return nil, fmt.Errorf("invalid code") + } + return PublicKeyFromX509DER(bytes) +} + +// PublicKeyFromX509DER decodes an X.509 DER (binary) encoded public key. +func PublicKeyFromX509DER(bytes []byte) (*PublicKey, error) { + pub, err := x509.ParsePKIXPublicKey(bytes) + if err != nil { + return nil, err + } + return &PublicKey{k: pub.(*rsa.PublicKey)}, nil +} + +// PublicKeyFromX509PEM decodes an X.509 PEM (string) encoded public key. +func PublicKeyFromX509PEM(str string) (*PublicKey, error) { + block, _ := pem.Decode([]byte(str)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + if block.Type != pemPubBlockType { + return nil, fmt.Errorf("incorrect PEM block type") + } + return PublicKeyFromX509DER(block.Bytes) +} + +func (p *PublicKey) BitLen() int { + return p.k.N.BitLen() +} + +func (p *PublicKey) NBytes() []byte { + return p.k.N.Bytes() +} + +func (p *PublicKey) EBytes() []byte { + return new(big.Int).SetInt64(int64(p.k.E)).Bytes() +} + +func (p *PublicKey) Equal(other crypto.PublicKey) bool { + if other, ok := other.(*PublicKey); ok { + return p.k.Equal(other.k) + } + return false +} + +func (p *PublicKey) ToPublicKeyMultibase() string { + bytes := p.ToX509DER() + return helpers.PublicKeyMultibaseEncode(MultibaseCode, bytes) +} + +func (p *PublicKey) ToX509DER() []byte { + res, _ := x509.MarshalPKIXPublicKey(p.k) + return res +} + +func (p *PublicKey) ToX509PEM() string { + der := p.ToX509DER() + return string(pem.EncodeToMemory(&pem.Block{ + Type: pemPubBlockType, + Bytes: der, + })) +} + +func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.SigningOption) bool { + return false +} + +func (p *PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool { + return false +}