From 3371b7c65ce10eb1217e7e8218736e0168e8293e Mon Sep 17 00:00:00 2001 From: Sun Yimin Date: Wed, 11 Dec 2024 08:36:55 +0800 Subject: [PATCH] cfca csr with tmp publickey and parse returned encryption key #286 --- cfca/pkcs10.go | 70 +++++++++++++++++++++++++-- cfca/pkcs10_test.go | 103 ++++++++++++++++++++++++++++++++++++++-- cfca/pkcs12_sm2.go | 9 ++-- smx509/cfca_csr.go | 19 +++----- smx509/cfca_csr_test.go | 10 ++-- 5 files changed, 181 insertions(+), 30 deletions(-) diff --git a/cfca/pkcs10.go b/cfca/pkcs10.go index 4dc6353..e0ac670 100644 --- a/cfca/pkcs10.go +++ b/cfca/pkcs10.go @@ -5,9 +5,16 @@ package cfca import ( + "bytes" + "crypto/ecdsa" "crypto/x509" + "encoding/asn1" + "encoding/base64" + "errors" "io" + "strconv" + "github.com/emmansun/gmsm/sm2" "github.com/emmansun/gmsm/smx509" ) @@ -15,13 +22,70 @@ type CertificateRequest = smx509.CertificateRequestCFCA // CreateCertificateRequest creates a new certificate request based on a template. // The following members of template are used: Subject. -// The certPriv is the private key for the certificate, and the tmpPriv is the temporary private key for returning encryption key decryption. +// The certPriv is the private key for the certificate, and the tmpPub is the temporary public key for returning encryption key decryption. // The challenge password is basically a shared-secret nonce between you and CFCA, embedded in the CSR. -func CreateCertificateRequest(rand io.Reader, template *x509.CertificateRequest, certPriv, tmpPriv any, challengePassword string) ([]byte, error) { - return smx509.CreateCFCACertificateRequest(rand, template, certPriv, tmpPriv, challengePassword) +func CreateCertificateRequest(rand io.Reader, template *x509.CertificateRequest, certPriv, tmpPub any, challengePassword string) ([]byte, error) { + return smx509.CreateCFCACertificateRequest(rand, template, certPriv, tmpPub, challengePassword) } // ParseCertificateRequest parses a certificate request from the given DER data. func ParseCertificateRequest(der []byte) (*CertificateRequest, error) { return smx509.ParseCFCACertificateRequest(der) } + +const encryptedEncKeyPrefix = "0000000000000001000000000000000100000000000000000000000000000000" + +type encryptedPrivateKeyInfo struct { + Version int `asn1:"default:1"` + EncryptedKey []byte +} + +// ParseEncryptionPrivateKey parses an encrypted SM2 private key from the given data. +// The data is expected to be in the format of "0000000000000001000000000000000100000000000000000000000000000000...". +// If the data is not in this format, it will be treated as base64 encoded data directly. +func ParseEncryptionPrivateKey(tmpPriv *sm2.PrivateKey, data []byte) (*sm2.PrivateKey, error) { + if len(data) < 268 { + return nil, errors.New("cfca: invalid encrypted private key data") + } + encodedKeyPart := data + if bytes.HasPrefix(data, []byte(encryptedEncKeyPrefix)) { + retLen, err := strconv.Atoi(string(data[64:80])) + if err != nil { + return nil, err + } + if retLen != len(data[80:]) { + return nil, errors.New("cfca: invalid encrypted private key data") + } + encodedKeyPart = data[80:] + } + // remove all commas ONLY now. If there are other non-base64 characters, the base64 decoder will fail. + encodedKeyPart = bytes.ReplaceAll(encodedKeyPart, []byte{44}, []byte{}) + der, err := base64.StdEncoding.DecodeString(string(encodedKeyPart)) + if err != nil { + return nil, err + } + var keyInfo encryptedPrivateKeyInfo + if _, err := asn1.Unmarshal(der, &keyInfo); err != nil { + return nil, err + } + var ret []byte + if ret, err = tmpPriv.Decrypt(nil, append([]byte{0x04}, keyInfo.EncryptedKey...), nil); err != nil { + return nil, errors.New("cfca: failed to decrypt the private key, possibly due to incorrect key data") + } + // X || Y || D + if len(ret) != 96 { + return nil, errors.New("cfca: invalid decrypted private key data") + } + var priv *sm2.PrivateKey + if priv, err = sm2.NewPrivateKey(ret[64:]); err != nil { + return nil, err + } + var pub *ecdsa.PublicKey + if pub, err = sm2.NewPublicKey(append([]byte{0x04}, ret[:64]...)); err != nil { + return nil, err + } + if !pub.Equal(&priv.PublicKey) { + return nil, errors.New("cfca: key pair mismatch, possibly due to incorrect key data or corruption") + } + return priv, nil +} diff --git a/cfca/pkcs10_test.go b/cfca/pkcs10_test.go index 9ced083..61659b9 100644 --- a/cfca/pkcs10_test.go +++ b/cfca/pkcs10_test.go @@ -10,6 +10,7 @@ import ( "crypto/rand" "crypto/x509" "crypto/x509/pkix" + "encoding/hex" "testing" "github.com/emmansun/gmsm/sm2" @@ -41,18 +42,18 @@ func TestCreateCertificateRequest(t *testing.T) { t.Fatal("certificate private key does not implement crypto.Signer") } _, err = CreateCertificateRequest(random, template, certKey, "", "") - if err == nil || err.Error() != "x509: tmp private key does not implement crypto.Signer" { - t.Fatal("tmp private key does not implement crypto.Signer") + if err == nil || err.Error() != "x509: only SM2 public key is supported" { + t.Fatal("only SM2 public key is supported") } - _, err = CreateCertificateRequest(random, template, certKey, invalidTmpKey, "") + _, err = CreateCertificateRequest(random, template, certKey, invalidTmpKey.Public(), "") if err == nil || err.Error() != "x509: only SM2 public key is supported" { t.Fatal("only SM2 public key is supported") } - _, err = CreateCertificateRequest(random, template, certKey, tmpKey, "") + _, err = CreateCertificateRequest(random, template, certKey, tmpKey.Public(), "") if err == nil || err.Error() != "x509: challenge password is required" { t.Fatal("challenge password is required") } - csrDer, err := CreateCertificateRequest(random, template, certKey, tmpKey, "111111") + csrDer, err := CreateCertificateRequest(random, template, certKey, tmpKey.Public(), "111111") if err != nil { t.Fatal(err) } @@ -73,3 +74,95 @@ func TestCreateCertificateRequest(t *testing.T) { t.Fatal("tmp public key not match") } } + +func TestParseEncryptionPrivateKey(t *testing.T) { + cases := []struct { + encKeyHex string + tmpKeyHex string + encryptedKey string + wantError bool + errorMsg string + }{ + { // with prefix, without delimiter + "f6e02c941a0dfdac58d8b3b1bc1bd136f179741b7465ebc7b0b25bb381840a3b", + "cacece36cac24aab94e52bcd5c0f552c95028f2856053135a1e47510b4c307ba", + "00000000000000010000000000000001000000000000000000000000000000000000000000000268MIHGAgEBBIHArhtKwTVT8dPEkykVRpvQNMxHv/yeqtaKZiSp2MbjcqMZtPfKW8IatiIPPitNhQtU5C7gMbsUxgf5Yo16vDSXdoWqoOOaes2pEJwmXWZI55lMMWc168WgzQ82fmMi05Vhlw9HNjGI3azE6MS5/ujSNGLZ0qAAmLnBiHlXFAXXAWRiy9MxZKwF4xKn6qMaKmkqbYmTbBbEJEhzJBmu0IJ1kNDcTFirAyapghHSw267erSUwsHjkQis9mKYpzGied0E", + false, + "", + }, + { // without prefix, without delimiter + "f6e02c941a0dfdac58d8b3b1bc1bd136f179741b7465ebc7b0b25bb381840a3b", + "cacece36cac24aab94e52bcd5c0f552c95028f2856053135a1e47510b4c307ba", + "MIHGAgEBBIHArhtKwTVT8dPEkykVRpvQNMxHv/yeqtaKZiSp2MbjcqMZtPfKW8IatiIPPitNhQtU5C7gMbsUxgf5Yo16vDSXdoWqoOOaes2pEJwmXWZI55lMMWc168WgzQ82fmMi05Vhlw9HNjGI3azE6MS5/ujSNGLZ0qAAmLnBiHlXFAXXAWRiy9MxZKwF4xKn6qMaKmkqbYmTbBbEJEhzJBmu0IJ1kNDcTFirAyapghHSw267erSUwsHjkQis9mKYpzGied0E", + false, + "", + }, + { // with prefix, with delimiter + "f6e02c941a0dfdac58d8b3b1bc1bd136f179741b7465ebc7b0b25bb381840a3b", + "cacece36cac24aab94e52bcd5c0f552c95028f2856053135a1e47510b4c307ba", + "00000000000000010000000000000001000000000000000000000000000000000000000000000273MIHGAgEBBIHArhtKwTVT8dPEkykVRpvQNMxHv/yeqtaKZiSp2MbjcqMZtPfKW8Ia,tiIPPitNhQtU5C7gMbsUxgf5Yo16vDSXdoWqoOOaes2pEJwmXWZI55lMMWc168Wg,zQ82fmMi05Vhlw9HNjGI3azE6MS5/ujSNGLZ0qAAmLnBiHlXFAXXAWRiy9MxZKwF,4xKn6qMaKmkqbYmTbBbEJEhzJBmu0IJ1kNDcTFirAyapghHSw267erSUwsHjkQis,9mKYpzGied0E,", + false, + "", + }, + { // too short + "f6e02c941a0dfdac58d8b3b1bc1bd136f179741b7465ebc7b0b25bb381840a3b", + "cacece36cac24aab94e52bcd5c0f552c95028f2856053135a1e47510b4c307ba", + "000000000000000100000000000000010", + true, + "cfca: invalid encrypted private key data", + }, + { // length not match + "f6e02c941a0dfdac58d8b3b1bc1bd136f179741b7465ebc7b0b25bb381840a3b", + "cacece36cac24aab94e52bcd5c0f552c95028f2856053135a1e47510b4c307ba", + "00000000000000010000000000000001000000000000000000000000000000000000000000000273MIHGAgEBBIHArhtKwTVT8dPEkykVRpvQNMxHv/yeqtaKZiSp2MbjcqMZtPfKW8IatiIPPitNhQtU5C7gMbsUxgf5Yo16vDSXdoWqoOOaes2pEJwmXWZI55lMMWc168WgzQ82fmMi05Vhlw9HNjGI3azE6MS5/ujSNGLZ0qAAmLnBiHlXFAXXAWRiy9MxZKwF4xKn6qMaKmkqbYmTbBbEJEhzJBmu0IJ1kNDcTFirAyapghHSw267erSUwsHjkQis9mKYpzGied0E", + true, + "cfca: invalid encrypted private key data", + }, + { // with prefix, with invalid delimiter + "f6e02c941a0dfdac58d8b3b1bc1bd136f179741b7465ebc7b0b25bb381840a3b", + "cacece36cac24aab94e52bcd5c0f552c95028f2856053135a1e47510b4c307ba", + "00000000000000010000000000000001000000000000000000000000000000000000000000000274MIHGAgEBBIHArhtKwTVT8dPEkykVRpvQNMxHv/yeqtaKZiSp2MbjcqMZtPfKW8Ia, tiIPPitNhQtU5C7gMbsUxgf5Yo16vDSXdoWqoOOaes2pEJwmXWZI55lMMWc168Wg,zQ82fmMi05Vhlw9HNjGI3azE6MS5/ujSNGLZ0qAAmLnBiHlXFAXXAWRiy9MxZKwF,4xKn6qMaKmkqbYmTbBbEJEhzJBmu0IJ1kNDcTFirAyapghHSw267erSUwsHjkQis,9mKYpzGied0E,", + true, + "illegal base64 data at input byte 64", + }, + { // invalid base64 + "f6e02c941a0dfdac58d8b3b1bc1bd136f179741b7465ebc7b0b25bb381840a3b", + "cacece36cac24aab94e52bcd5c0f552c95028f2856053135a1e47510b4c307ba", + "NIHGAgEBBIHArhtKwTVT8dPEkykVRpvQNMxHv/yeqtaKZiSp2MbjcqMZtPfKW8IatiIPPitNhQtU5C7gMbsUxgf5Yo16vDSXdoWqoOOaes2pEJwmXWZI55lMMWc168WgzQ82fmMi05Vhlw9HNjGI3azE6MS5/ujSNGLZ0qAAmLnBiHlXFAXXAWRiy9MxZKwF4xKn6qMaKmkqbYmTbBbEJEhzJBmu0IJ1kNDcTFirAyapghHSw267erSUwsHjkQis9mKYpzGied0E", + true, + "asn1: structure error: tags don't match (16 vs {class:0 tag:20 length:198 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:0 set:false omitEmpty:false} encryptedPrivateKeyInfo @3", + }, + { // with prefix, with delimiter, invalid length string + "f6e02c941a0dfdac58d8b3b1bc1bd136f179741b7465ebc7b0b25bb381840a3b", + "cacece36cac24aab94e52bcd5c0f552c95028f2856053135a1e47510b4c307ba", + "00000000000000010000000000000001000000000000000000000000000000000000000000000/73MIHGAgEBBIHArhtKwTVT8dPEkykVRpvQNMxHv/yeqtaKZiSp2MbjcqMZtPfKW8Ia,tiIPPitNhQtU5C7gMbsUxgf5Yo16vDSXdoWqoOOaes2pEJwmXWZI55lMMWc168Wg,zQ82fmMi05Vhlw9HNjGI3azE6MS5/ujSNGLZ0qAAmLnBiHlXFAXXAWRiy9MxZKwF,4xKn6qMaKmkqbYmTbBbEJEhzJBmu0IJ1kNDcTFirAyapghHSw267erSUwsHjkQis,9mKYpzGied0E,", + true, + "strconv.Atoi: parsing \"0000000000000/73\": invalid syntax", + }, + } + for _, c := range cases { + encKey, _ := hex.DecodeString(c.encKeyHex) + tmpKey, _ := hex.DecodeString(c.tmpKeyHex) + tmpSM2Key, err := sm2.NewPrivateKey(tmpKey) + if err != nil { + t.Fatal(err) + } + targetEncKey, err := sm2.NewPrivateKey(encKey) + if err != nil { + t.Fatal(err) + } + gotKey, err := ParseEncryptionPrivateKey(tmpSM2Key, []byte(c.encryptedKey)) + if c.wantError { + if err == nil || err.Error() != c.errorMsg { + t.Fatalf("expected error %v, got %v", c.errorMsg, err) + } + continue + } + if err != nil { + t.Fatal(err) + } + if !gotKey.Equal(targetEncKey) { + t.Fatalf("decrypted key not match") + } + } +} diff --git a/cfca/pkcs12_sm2.go b/cfca/pkcs12_sm2.go index b985b53..dd2237f 100644 --- a/cfca/pkcs12_sm2.go +++ b/cfca/pkcs12_sm2.go @@ -70,11 +70,10 @@ func ParseSM2(password, data []byte) (*sm2.PrivateKey, *smx509.Certificate, erro } d := new(big.Int).SetBytes(pk) // here we do NOT check if the d is in (0, N) or not // Create private key from *big.Int - prvKey := new(sm2.PrivateKey) - prvKey.Curve = sm2.P256() - prvKey.D = d - prvKey.PublicKey.X, prvKey.PublicKey.Y = prvKey.ScalarBaseMult(prvKey.D.Bytes()) - + prvKey, err := sm2.NewPrivateKeyFromInt(d) + if err != nil { + return nil, nil, err + } cert, err := smx509.ParseCertificate(keys.Certificate.Content) if err != nil { return nil, nil, err diff --git a/smx509/cfca_csr.go b/smx509/cfca_csr.go index 5355383..930cd22 100644 --- a/smx509/cfca_csr.go +++ b/smx509/cfca_csr.go @@ -29,11 +29,11 @@ var ( // - SignatureAlgorithm // - Subject // -// The certPriv is the private key for the certificate, and the tmpPriv is the temporary private key for returning encryption key decryption. +// The certPriv is the private key for the certificate, and the tmpPub is the temporary private key for returning encryption key decryption. // The challenge password is basically a shared-secret nonce between you and CFCA, embedded in the CSR, // which the issuer may use to authenticate you should that ever be needed. // The template is the certificate request template, we just use Subject now. -func CreateCFCACertificateRequest(rand io.Reader, template *x509.CertificateRequest, priv, tmpPriv any, challengePassword string) ([]byte, error) { +func CreateCFCACertificateRequest(rand io.Reader, template *x509.CertificateRequest, priv, tmpPub any, challengePassword string) ([]byte, error) { key, ok := priv.(crypto.Signer) if !ok { return nil, errors.New("x509: certificate private key does not implement crypto.Signer") @@ -52,8 +52,8 @@ func CreateCFCACertificateRequest(rand io.Reader, template *x509.CertificateRequ var rawAttributes []asn1.RawValue // Add the temporary public key and challenge password if requested. - if tmpPriv != nil { - rawAttributes, err = buildTmpPublicKeyAttr(rawAttributes, tmpPriv) + if tmpPub != nil { + rawAttributes, err = buildTmpPublicKeyAttr(rawAttributes, tmpPub) if err != nil { return nil, err } @@ -130,18 +130,13 @@ func buildChallengePasswordAttr(rawAttributes []asn1.RawValue, challengePassword return append(rawAttributes, rawValue), nil } -func buildTmpPublicKeyAttr(rawAttributes []asn1.RawValue, tmpPriv any) ([]asn1.RawValue, error) { - key, ok := tmpPriv.(crypto.Signer) - if !ok { - return nil, errors.New("x509: tmp private key does not implement crypto.Signer") - } +func buildTmpPublicKeyAttr(rawAttributes []asn1.RawValue, tmpPub crypto.PublicKey) ([]asn1.RawValue, error) { var publicKeyBytes [136]byte copy(publicKeyBytes[:], tmpPublicKeyPrefix) - pub := key.Public() - if !sm2.IsSM2PublicKey(pub) { + if !sm2.IsSM2PublicKey(tmpPub) { return nil, errors.New("x509: only SM2 public key is supported") } - ecPub, _ := pub.(*ecdsa.PublicKey) + ecPub, _ := tmpPub.(*ecdsa.PublicKey) ecPub.X.FillBytes(publicKeyBytes[8:40]) ecPub.Y.FillBytes(publicKeyBytes[72:104]) b, _ := asn1.Marshal(publicKeyBytes[:]) diff --git a/smx509/cfca_csr_test.go b/smx509/cfca_csr_test.go index 4df658e..71e8071 100644 --- a/smx509/cfca_csr_test.go +++ b/smx509/cfca_csr_test.go @@ -41,18 +41,18 @@ func TestCreateCFCACertificateRequest(t *testing.T) { t.Fatal("certificate private key does not implement crypto.Signer") } _, err = CreateCFCACertificateRequest(random, template, certKey, "", "") - if err == nil || err.Error() != "x509: tmp private key does not implement crypto.Signer" { - t.Fatal("tmp private key does not implement crypto.Signer") + if err == nil || err.Error() != "x509: only SM2 public key is supported" { + t.Fatal("only SM2 public key is supported") } - _, err = CreateCFCACertificateRequest(random, template, certKey, invalidTmpKey, "") + _, err = CreateCFCACertificateRequest(random, template, certKey, invalidTmpKey.Public(), "") if err == nil || err.Error() != "x509: only SM2 public key is supported" { t.Fatal("only SM2 public key is supported") } - _, err = CreateCFCACertificateRequest(random, template, certKey, tmpKey, "") + _, err = CreateCFCACertificateRequest(random, template, certKey, tmpKey.Public(), "") if err == nil || err.Error() != "x509: challenge password is required" { t.Fatal("challenge password is required") } - csrDer, err := CreateCFCACertificateRequest(random, template, certKey, tmpKey, "111111") + csrDer, err := CreateCFCACertificateRequest(random, template, certKey, tmpKey.Public(), "111111") if err != nil { t.Fatal(err) }