Christopher Boumenot uploaded a change:
https://go-review.googlesource.com/20075
crypto: PKCS#12 encode support
Fixes golang/go#14125
Change-Id: Ic56b45a0fc4d5a5fa38d2b3ea563d50e59fb1fbb
---
M pkcs12/bmp-string_test.go
M pkcs12/crypto.go
M pkcs12/crypto_test.go
M pkcs12/errors.go
M pkcs12/mac.go
M pkcs12/pkcs12.go
M pkcs12/pkcs12_test.go
A pkcs12/pkcs8.go
A pkcs12/pkcs8_test.go
M pkcs12/safebags.go
A pkcs12/safebags_test.go
11 files changed, 742 insertions(+), 19 deletions(-)
diff --git a/pkcs12/bmp-string_test.go b/pkcs12/bmp-string_test.go
index 7fca55f..527449f 100644
--- a/pkcs12/bmp-string_test.go
+++ b/pkcs12/bmp-string_test.go
@@ -22,6 +22,14 @@
{"\u2115 - Double-struck
N", "21150020002d00200044006f00750062006c0065002d00730074007200750063006b0020004e0000",
false},
// any character outside the BMP should trigger an error.
{"\U0001f000 East wind (Mahjong)", "", true},
+ // odd length string
+}
+
+func TestBMPStringDecode(t *testing.T) {
+ _, err := decodeBMPString([]byte("a"))
+ if err == nil {
+ t.Fatalf("expected decode to fail, but it succeeded")
+ }
}
func TestBMPString(t *testing.T) {
diff --git a/pkcs12/crypto.go b/pkcs12/crypto.go
index 4bd4470..3d68b44 100644
--- a/pkcs12/crypto.go
+++ b/pkcs12/crypto.go
@@ -8,11 +8,18 @@
"bytes"
"crypto/cipher"
"crypto/des"
+ "crypto/rand"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
+ "io"
"
golang.org/x/crypto/pkcs12/internal/rc2"
+)
+
+const (
+ pbeIterationCount = 2048
+ pbeSaltSizeBytes = 8
)
var (
@@ -124,6 +131,36 @@
return
}
+func pad(src []byte, blockSize int) []byte {
+ paddingLength := blockSize - len(src)%blockSize
+ paddingText := bytes.Repeat([]byte{byte(paddingLength)}, paddingLength)
+ return append(src, paddingText...)
+}
+
+func pbEncrypt(plainText, salt, password []byte, iterations int)
(cipherText []byte, err error) {
+ _, err = io.ReadFull(rand.Reader, salt)
+ if err != nil {
+ return nil, errors.New("pkcs12: failed to create a random salt value: "
+ err.Error())
+ }
+
+ cipherType := shaWithTripleDESCBC{}
+ key := cipherType.deriveKey(salt, password, iterations)
+ iv := cipherType.deriveIV(salt, password, iterations)
+
+ block, err := cipherType.create(key)
+ if err != nil {
+ return nil, errors.New("pkcs12: failed to create a block cipher: " +
err.Error())
+ }
+
+ paddedPlainText := pad(plainText, block.BlockSize())
+
+ encrypter := cipher.NewCBCEncrypter(block, iv)
+ cipherText = make([]byte, len(paddedPlainText))
+ encrypter.CryptBlocks(cipherText, paddedPlainText)
+
+ return cipherText, nil
+}
+
// decryptable abstracts a object that contains ciphertext.
type decryptable interface {
Algorithm() pkix.AlgorithmIdentifier
diff --git a/pkcs12/crypto_test.go b/pkcs12/crypto_test.go
index eb4dae8..db834fd 100644
--- a/pkcs12/crypto_test.go
+++ b/pkcs12/crypto_test.go
@@ -6,8 +6,10 @@
import (
"bytes"
+ "crypto/rand"
"crypto/x509/pkix"
"encoding/asn1"
+ "io"
"testing"
)
@@ -79,17 +81,9 @@
}
func TestPbDecrypt(t *testing.T) {
+ salt := []byte("\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8")
for i, test := range pbDecryptTests {
- decryptable := testDecryptable{
- data:
test.in,
- algorithm: pkix.AlgorithmIdentifier{
- Algorithm: sha1WithTripleDES,
- Parameters: pbeParams{
- Salt: []byte("\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8"),
- Iterations: 4096,
- }.RawASN1(),
- },
- }
+ decryptable := makeTestDecryptable(
test.in, salt)
password, _ := bmpString("sesame")
plaintext, err := pbDecrypt(decryptable, password)
@@ -104,6 +98,54 @@
}
}
+func TestRoundTripPkc12EncryptDecrypt(t *testing.T) {
+ salt := []byte{0xfe, 0xee, 0xfa, 0xce}
+ password := salt
+
+ // Sweep the possible padding lengths
+ for i := 25; i < 9; i++ {
+ bs := make([]byte, i)
+ _, err := io.ReadFull(rand.Reader, bs)
+ if err != nil {
+ t.Fatalf("failed to read: %s", err)
+ }
+
+ cipherText, err := pbEncrypt(bs, salt, password, 4096)
+ if err != nil {
+ t.Fatalf("failed to encrypt: %s\n", err)
+ }
+
+ if len(cipherText)%8 != 0 {
+ t.Fatalf("plain text was not padded as expected")
+ }
+
+ decryptable := makeTestDecryptable(cipherText, salt)
+ plainText, err := pbDecrypt(decryptable, password)
+ if err != nil {
+ t.Fatalf("failed to decrypt: %s\n", err)
+ }
+
+ if !bytes.Equal(bs, plainText) {
+ t.Fatalf("got %x, but wanted %x", bs, plainText)
+ }
+ }
+}
+
+func makeTestDecryptable(bytes, salt []byte) testDecryptable {
+ decryptable := testDecryptable{
+ data: bytes,
+ algorithm: pkix.AlgorithmIdentifier{
+ Algorithm: sha1WithTripleDES,
+ Parameters: pbeParams{
+ Salt: salt,
+ Iterations: 4096,
+ }.RawASN1(),
+ },
+ }
+
+ return decryptable
+}
+
type testDecryptable struct {
data []byte
algorithm pkix.AlgorithmIdentifier
diff --git a/pkcs12/errors.go b/pkcs12/errors.go
index 7377ce6..36ad6e9 100644
--- a/pkcs12/errors.go
+++ b/pkcs12/errors.go
@@ -17,7 +17,12 @@
// NotImplementedError indicates that the input is not currently supported.
type NotImplementedError string
+type EncodeError string
func (e NotImplementedError) Error() string {
return "pkcs12: " + string(e)
}
+
+func (e EncodeError) Error() string {
+ return "pkcs12: encode error: " + string(e)
+}
diff --git a/pkcs12/mac.go b/pkcs12/mac.go
index 5f38aa7..76ad0cd 100644
--- a/pkcs12/mac.go
+++ b/pkcs12/mac.go
@@ -32,14 +32,19 @@
return NotImplementedError("unknown digest algorithm: " +
macData.Mac.Algorithm.Algorithm.String())
}
- key := pbkdf(sha1Sum, 20, 64, macData.MacSalt, password,
macData.Iterations, 3, 20)
-
- mac := hmac.New(sha1.New, key)
- mac.Write(message)
- expectedMAC := mac.Sum(nil)
+ expectedMAC := computeMac(message, macData.Iterations, macData.MacSalt,
password)
if !hmac.Equal(macData.Mac.Digest, expectedMAC) {
return ErrIncorrectPassword
}
return nil
}
+
+func computeMac(message []byte, iterations int, salt, password []byte)
[]byte {
+ key := pbkdf(sha1Sum, 20, 64, salt, password, iterations, 3, 20)
+
+ mac := hmac.New(sha1.New, key)
+ mac.Write(message)
+
+ return mac.Sum(nil)
+}
diff --git a/pkcs12/pkcs12.go b/pkcs12/pkcs12.go
index ad6341e..8f733cf 100644
--- a/pkcs12/pkcs12.go
+++ b/pkcs12/pkcs12.go
@@ -11,6 +11,7 @@
import (
"crypto/ecdsa"
+ "crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
@@ -18,6 +19,7 @@
"encoding/hex"
"encoding/pem"
"errors"
+ "io"
)
var (
@@ -27,12 +29,18 @@
oidFriendlyName = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1,
9, 20})
oidLocalKeyID = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1,
9, 21})
oidMicrosoftCSPName = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 311,
17, 1})
+
+ localKeyId = []byte{0x01, 0x00, 0x00, 0x00}
)
type pfxPdu struct {
Version int
AuthSafe contentInfo
MacData macData `asn1:"optional"`
+}
+
+type dataContent struct {
+ data []byte `asn1:"tag:4"`
}
type contentInfo struct {
@@ -142,7 +150,7 @@
return nil, err
}
block.Bytes = certsData
- case bag.Id.Equal(oidPKCS8ShroundedKeyBag):
+ case bag.Id.Equal(oidPKCS8ShroudedKeyBag):
block.Type = privateKeyType
key, err := decodePkcs8ShroudedKeyBag(bag.Value.Bytes, password)
@@ -242,11 +250,10 @@
}
certificate = certs[0]
- case bag.Id.Equal(oidPKCS8ShroundedKeyBag):
+ case bag.Id.Equal(oidPKCS8ShroudedKeyBag):
if privateKey != nil {
err = errors.New("pkcs12: expected exactly one key bag")
}
-
if privateKey, err = decodePkcs8ShroudedKeyBag(bag.Value.Bytes,
encodedPassword); err != nil {
return nil, nil, err
}
@@ -263,8 +270,192 @@
return
}
+func getLocalKeyId(id []byte) (attribute pkcs12Attribute, err error) {
+ octetString := asn1.RawValue{Tag: 4, Class: 0, IsCompound: false, Bytes:
id}
+ bytes, err := asn1.Marshal(octetString)
+ if err != nil {
+ return
+ }
+
+ attribute = pkcs12Attribute{
+ Id: oidLocalKeyID,
+ Value: asn1.RawValue{Tag: 17, Class: 0, IsCompound: true, Bytes: bytes},
+ }
+
+ return attribute, nil
+}
+
+func convertToRawVal(val interface{}) (raw asn1.RawValue, err error) {
+ bytes, err := asn1.Marshal(val)
+ if err != nil {
+ return
+ }
+
+ _, err = asn1.Unmarshal(bytes, &raw)
+ return raw, nil
+}
+
+func makeSafeBags(oid asn1.ObjectIdentifier, value []byte) ([]safeBag,
error) {
+ attribute, err := getLocalKeyId(localKeyId)
+
+ if err != nil {
+ return nil, EncodeError("local key id: " + err.Error())
+ }
+
+ bag := make([]safeBag, 1)
+ bag[0] = safeBag{
+ Id: oid,
+ Value: asn1.RawValue{Tag: 0, Class: 2, IsCompound: true, Bytes:
value},
+ Attributes: []pkcs12Attribute{attribute},
+ }
+
+ return bag, nil
+}
+
+func makeCertBagContentInfo(derBytes []byte) (*contentInfo, error) {
+ certBag1 := certBag{
+ Id: oidCertTypeX509Certificate,
+ Data: derBytes,
+ }
+
+ bytes, err := asn1.Marshal(certBag1)
+ if err != nil {
+ return nil, EncodeError("encoding cert bag: " + err.Error())
+ }
+
+ certSafeBags, err := makeSafeBags(oidCertBag, bytes)
+ if err != nil {
+ return nil, EncodeError("safe bags: " + err.Error())
+ }
+
+ return makeContentInfo(certSafeBags)
+}
+
+func makeShroudedKeyBagContentInfo(privateKey interface{}, password
[]byte) (*contentInfo, error) {
+ shroudedKeyBagBytes, err := encodePkcs8ShroudedKeyBag(privateKey,
password)
+ if err != nil {
+ return nil, EncodeError("encode PKCS#8 shrouded key bag: " + err.Error())
+ }
+
+ safeBags, err := makeSafeBags(oidPKCS8ShroudedKeyBag, shroudedKeyBagBytes)
+ if err != nil {
+ return nil, EncodeError("safe bags: " + err.Error())
+ }
+
+ return makeContentInfo(safeBags)
+}
+
+func makeContentInfo(val interface{}) (*contentInfo, error) {
+ fullBytes, err := asn1.Marshal(val)
+ if err != nil {
+ return nil, EncodeError("contentInfo raw value marshal: " + err.Error())
+ }
+
+ octetStringVal := asn1.RawValue{Tag: 4, Class: 0, IsCompound: false,
Bytes: fullBytes}
+ octetStringFullBytes, err := asn1.Marshal(octetStringVal)
+ if err != nil {
+ return nil, EncodeError("raw contentInfo to octet string: " +
err.Error())
+ }
+
+ contentInfo := contentInfo{ContentType: oidDataContentType}
+ contentInfo.Content = asn1.RawValue{Tag: 0, Class: 2, IsCompound: true,
Bytes: octetStringFullBytes}
+
+ return &contentInfo, nil
+}
+
+func makeContentInfos(derBytes []byte, privateKey interface{}, password
[]byte) ([]contentInfo, error) {
+ shroudedKeyContentInfo, err := makeShroudedKeyBagContentInfo(privateKey,
password)
+ if err != nil {
+ return nil, EncodeError("shrouded key content info: " + err.Error())
+ }
+
+ certBagContentInfo, err := makeCertBagContentInfo(derBytes)
+ if err != nil {
+ return nil, EncodeError("cert bag content info: " + err.Error())
+ }
+
+ contentInfos := make([]contentInfo, 2)
+ contentInfos[0] = *shroudedKeyContentInfo
+ contentInfos[1] = *certBagContentInfo
+
+ return contentInfos, nil
+}
+
+func makeSalt(saltByteCount int) ([]byte, error) {
+ salt := make([]byte, saltByteCount)
+ _, err := io.ReadFull(rand.Reader, salt)
+ return salt, err
+}
+
+// Encode converts a certificate and a private key to the PKCS#12 byte
stream format.
+//
+// derBytes is a DER encoded certificate.
+// privateKey is an RSA
+func Encode(derBytes []byte, privateKey interface{}, password string)
(pfxBytes []byte, err error) {
+ secret, err := bmpString(password)
+ if err != nil {
+ return nil, ErrIncorrectPassword
+ }
+
+ contentInfos, err := makeContentInfos(derBytes, privateKey, secret)
+ if err != nil {
+ return nil, err
+ }
+
+ // Marhsal []contentInfo so we can re-constitute the byte stream that will
+ // be suitable for computing the MAC
+ bytes, err := asn1.Marshal(contentInfos)
+ if err != nil {
+ return nil, err
+ }
+
+ // Unmarshal as an asn1.RawValue so, we can compute the MAC against
the .Bytes
+ var contentInfosRaw asn1.RawValue
+ err = unmarshal(bytes, &contentInfosRaw)
+ if err != nil {
+ return nil, err
+ }
+
+ authSafeContentInfo, err := makeContentInfo(contentInfosRaw)
+ if err != nil {
+ return nil, EncodeError("authSafe content info: " + err.Error())
+ }
+
+ salt, err := makeSalt(pbeSaltSizeBytes)
+ if err != nil {
+ return nil, EncodeError("salt value: " + err.Error())
+ }
+
+ // Compute the MAC for marshaled bytes of contentInfos, which includes the
+ // cert bag, and the shrouded key bag.
+ digest := computeMac(contentInfosRaw.FullBytes, pbeIterationCount, salt,
secret)
+
+ pfx := pfxPdu{
+ Version: 3,
+ AuthSafe: *authSafeContentInfo,
+ MacData: macData{
+ Iterations: pbeIterationCount,
+ MacSalt: salt,
+ Mac: digestInfo{
+ Algorithm: pkix.AlgorithmIdentifier{
+ Algorithm: oidSHA1,
+ },
+ Digest: digest,
+ },
+ },
+ }
+
+ bytes, err = asn1.Marshal(pfx)
+ if err != nil {
+ return nil, EncodeError("marshal PFX PDU: " + err.Error())
+ }
+
+ return bytes, err
+}
+
func getSafeContents(p12Data, password []byte) (bags []safeBag,
updatedPassword []byte, err error) {
pfx := new(pfxPdu)
+
if err := unmarshal(p12Data, pfx); err != nil {
return nil, nil, errors.New("pkcs12: error reading P12 data: " +
err.Error())
}
@@ -335,6 +526,7 @@
if err := unmarshal(data, &safeContents); err != nil {
return nil, nil, err
}
+
bags = append(bags, safeContents...)
}
diff --git a/pkcs12/pkcs12_test.go b/pkcs12/pkcs12_test.go
index 14dd2a6..2ac382a 100644
--- a/pkcs12/pkcs12_test.go
+++ b/pkcs12/pkcs12_test.go
@@ -5,11 +5,19 @@
package pkcs12
import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
"crypto/rsa"
"crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
+ "fmt"
+ "math/big"
"testing"
+ "time"
)
func TestPfx(t *testing.T) {
@@ -29,6 +37,61 @@
t.Errorf("expected common name to be %q, but found %q", commonName,
cert.Subject.CommonName)
}
}
+}
+
+func TestPfxRoundTriRsa(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ key := testPfxRoundTrip(t, privateKey)
+
+ actualPrivateKey, ok := key.(*rsa.PrivateKey)
+ if !ok {
+ t.Fatalf("failed to decode private key")
+ }
+
+ if privateKey.D.Cmp(actualPrivateKey.D) != 0 {
+ t.Errorf("priv.D")
+ }
+}
+
+func TestPfxRoundTriEcdsa(t *testing.T) {
+ privateKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ key := testPfxRoundTrip(t, privateKey)
+
+ actualPrivateKey, ok := key.(*ecdsa.PrivateKey)
+ if !ok {
+ t.Fatalf("failed to decode private key")
+ }
+
+ if privateKey.D.Cmp(actualPrivateKey.D) != 0 {
+ t.Errorf("priv.D")
+ }
+}
+
+func testPfxRoundTrip(t *testing.T, privateKey interface{}) interface{} {
+ certificateBytes, err := newCertificate("hostname", privateKey)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ bytes, err := Encode(certificateBytes, privateKey, "sesame")
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ key, _, err := Decode(bytes, "sesame")
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+
+ return key
}
func TestPEM(t *testing.T) {
@@ -86,6 +149,52 @@
_ = config
}
+func newCertificate(hostname string, privateKey interface{}) ([]byte,
error) {
+ t, _ := time.Parse("2006-01-02", "2016-01-01")
+ notBefore := t
+ notAfter := notBefore.Add(365 * 24 * time.Hour)
+
+ serialNumber, err := rand.Int(rand.Reader,
new(big.Int).Lsh(big.NewInt(1), 128))
+ if err != nil {
+ err := fmt.Errorf("Failed to Generate Serial Number: %v", err)
+ return nil, err
+ }
+
+ template := x509.Certificate{
+ SerialNumber: serialNumber,
+ Issuer: pkix.Name{
+ CommonName: hostname,
+ },
+ Subject: pkix.Name{
+ CommonName: hostname,
+ },
+
+ NotBefore: notBefore,
+ NotAfter: notAfter,
+
+ KeyUsage: x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ BasicConstraintsValid: true,
+ }
+
+ var publicKey interface{}
+ switch key := privateKey.(type) {
+ case *rsa.PrivateKey:
+ publicKey = key.Public()
+ case *ecdsa.PrivateKey:
+ publicKey = key.Public()
+ default:
+ panic(fmt.Sprintf("unsupported private key type: %T", privateKey))
+ }
+
+ derBytes, err := x509.CreateCertificate(rand.Reader, &template,
&template, publicKey, privateKey)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to Generate derBytes: " + err.Error())
+ }
+
+ return derBytes, nil
+}
+
var testdata = map[string]string{
// 'null' password test case
"Windows Azure Tools":
`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`,
diff --git a/pkcs12/pkcs8.go b/pkcs12/pkcs8.go
new file mode 100644
index 0000000..1eb8850
--- /dev/null
+++ b/pkcs12/pkcs8.go
@@ -0,0 +1,64 @@
+package pkcs12
+
+import (
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/asn1"
+ "errors"
+)
+
+// pkcs8 reflects an ASN.1, PKCS#8 PrivateKey. See
+//
ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-8/pkcs-8v1_2.asn
+// and RFC5208.
+type pkcs8 struct {
+ Version int
+ Algo pkix.AlgorithmIdentifier
+ PrivateKey []byte
+ // optional attributes omitted.
+}
+
+var (
+ oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
+ oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
+
+ nullAsn = asn1.RawValue{Tag: 5}
+)
+
+// marshalPKCS8PrivateKey converts a private key to PKCS#8 encoded form.
+// See
http://www.rsa.com/rsalabs/node.asp?id=2130 and RFC5208.
+func marshalPKCS8PrivateKey(key interface{}) (der []byte, err error) {
+ pkcs := pkcs8{
+ Version: 0,
+ }
+
+ switch key := key.(type) {
+ case *rsa.PrivateKey:
+ pkcs.Algo = pkix.AlgorithmIdentifier{
+ Algorithm: oidPublicKeyRSA,
+ Parameters: nullAsn,
+ }
+ pkcs.PrivateKey = x509.MarshalPKCS1PrivateKey(key)
+ case *ecdsa.PrivateKey:
+ bytes, err := x509.MarshalECPrivateKey(key)
+ if err != nil {
+ return nil, errors.New("x509: failed to marshal to PKCS#8: " +
err.Error())
+ }
+
+ pkcs.Algo = pkix.AlgorithmIdentifier{
+ Algorithm: oidPublicKeyECDSA,
+ Parameters: nullAsn,
+ }
+ pkcs.PrivateKey = bytes
+ default:
+ return nil, errors.New("x509: PKCS#8 only RSA and ECDSA private keys
supported")
+ }
+
+ bytes, err := asn1.Marshal(pkcs)
+ if err != nil {
+ return nil, errors.New("x509: failed to marshal to PKCS#8: " +
err.Error())
+ }
+
+ return bytes, nil
+}
diff --git a/pkcs12/pkcs8_test.go b/pkcs12/pkcs8_test.go
new file mode 100644
index 0000000..3c6a1d2
--- /dev/null
+++ b/pkcs12/pkcs8_test.go
@@ -0,0 +1,138 @@
+package pkcs12
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/asn1"
+ "testing"
+)
+
+func TestRoundTripPkcs8Rsa(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ bytes, err := marshalPKCS8PrivateKey(privateKey)
+ if err != nil {
+ t.Fatalf("failed to marshal private key: %s", err)
+ }
+
+ key, err := x509.ParsePKCS8PrivateKey(bytes)
+ if err != nil {
+ t.Fatalf("failed to parse private key: %s", err)
+ }
+
+ actualPrivateKey, ok := key.(*rsa.PrivateKey)
+ if !ok {
+ t.Fatalf("expected key to be of type *rsa.PrivateKey, but actual
was %T", key)
+ }
+
+ if actualPrivateKey.Validate() != nil {
+ t.Fatalf("private key did not validate")
+ }
+
+ if actualPrivateKey.N.Cmp(privateKey.N) != 0 {
+ t.Errorf("private key's N did not round trip")
+ }
+ if actualPrivateKey.D.Cmp(privateKey.D) != 0 {
+ t.Errorf("private key's D did not round trip")
+ }
+ if actualPrivateKey.E != privateKey.E {
+ t.Errorf("private key's E did not round trip")
+ }
+ if actualPrivateKey.Primes[0].Cmp(privateKey.Primes[0]) != 0 {
+ t.Errorf("private key's P did not round trip")
+ }
+ if actualPrivateKey.Primes[1].Cmp(privateKey.Primes[1]) != 0 {
+ t.Errorf("private key's Q did not round trip")
+ }
+}
+
+func TestRoundTripPkcs8Ecdsa(t *testing.T) {
+ privateKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ bytes, err := marshalPKCS8PrivateKey(privateKey)
+ if err != nil {
+ t.Fatalf("failed to marshal private key: %s", err)
+ }
+
+ key, err := x509.ParsePKCS8PrivateKey(bytes)
+ if err != nil {
+ t.Fatalf("failed to parse private key: %s", err)
+ }
+
+ actualPrivateKey, ok := key.(*ecdsa.PrivateKey)
+ if !ok {
+ t.Fatalf("expected key to be of type *ecdsa.PrivateKey, but actual
was %T", key)
+ }
+
+ // sanity check, not exhaustive
+ if actualPrivateKey.D.Cmp(privateKey.D) != 0 {
+ t.Errorf("private key's D did not round trip")
+ }
+ if actualPrivateKey.X.Cmp(privateKey.X) != 0 {
+ t.Errorf("private key's X did not round trip")
+ }
+ if actualPrivateKey.Y.Cmp(privateKey.Y) != 0 {
+ t.Errorf("private key's Y did not round trip")
+ }
+ if actualPrivateKey.Curve.Params().B.Cmp(privateKey.Curve.Params().B) !=
0 {
+ t.Errorf("private key's Curve.B did not round trip")
+ }
+}
+
+func TestNullParametersPkcs8Rsa(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ checkNullParameter(t, privateKey)
+}
+
+func TestNullParametersPkcs8Ecdsa(t *testing.T) {
+ privateKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ checkNullParameter(t, privateKey)
+}
+
+func checkNullParameter(t *testing.T, privateKey interface{}) {
+ bytes, err := marshalPKCS8PrivateKey(privateKey)
+ if err != nil {
+ t.Fatalf("failed to marshal private key: %s", err)
+ }
+
+ var pkcs pkcs8
+ rest, err := asn1.Unmarshal(bytes, &pkcs)
+ if err != nil {
+ t.Fatalf("failed to unmarshal PKCS#8: %s", err)
+ }
+
+ if len(rest) != 0 {
+ t.Fatalf("unexpected trailing bytes of len=%d, bytes=%x", len(rest),
rest)
+ }
+
+ // Only version == 0 is known and valid
+ if pkcs.Version != 0 {
+ t.Errorf("expected version=0, but actual=%d", pkcs.Version)
+ }
+
+ // ensure a NULL parameter is inserted
+ if pkcs.Algo.Parameters.Tag != 5 {
+ t.Errorf("expected parameters to be NULL, but actual tag=%d, class=%d,
isCompound=%d, bytes=%x",
+ pkcs.Algo.Parameters.Tag,
+ pkcs.Algo.Parameters.Class,
+ pkcs.Algo.Parameters.IsCompound,
+ pkcs.Algo.Parameters.Bytes)
+ }
+}
diff --git a/pkcs12/safebags.go b/pkcs12/safebags.go
index def1f7b..bbd1526 100644
--- a/pkcs12/safebags.go
+++ b/pkcs12/safebags.go
@@ -6,6 +6,7 @@
import (
"crypto/x509"
+ "crypto/x509/pkix"
"encoding/asn1"
"errors"
)
@@ -13,7 +14,7 @@
var (
// see
https://tools.ietf.org/html/rfc7292#appendix-D
oidCertTypeX509Certificate = asn1.ObjectIdentifier([]int{1, 2, 840,
113549, 1, 9, 22, 1})
- oidPKCS8ShroundedKeyBag = asn1.ObjectIdentifier([]int{1, 2, 840,
113549, 1, 12, 10, 1, 2})
+ oidPKCS8ShroudedKeyBag = asn1.ObjectIdentifier([]int{1, 2, 840,
113549, 1, 12, 10, 1, 2})
oidCertBag = asn1.ObjectIdentifier([]int{1, 2, 840,
113549, 1, 12, 10, 1, 3})
)
@@ -22,6 +23,53 @@
Data []byte `asn1:"tag:0,explicit"`
}
+func getAlgorithmParams(salt []byte, iterations int) (asn1.RawValue,
error) {
+ params := pbeParams{
+ Salt: salt,
+ Iterations: iterations,
+ }
+
+ return convertToRawVal(params)
+}
+
+func encodePkcs8ShroudedKeyBag(privateKey interface{}, password []byte)
(bytes []byte, err error) {
+ privateKeyBytes, err := marshalPKCS8PrivateKey(privateKey)
+
+ if err != nil {
+ return nil, errors.New("pkcs12: error encoding PKCS#8 private key: " +
err.Error())
+ }
+
+ salt, err := makeSalt(pbeSaltSizeBytes)
+ if err != nil {
+ return nil, errors.New("pkcs12: error creating PKCS#8 salt: " +
err.Error())
+ }
+
+ pkData, err := pbEncrypt(privateKeyBytes, salt, password,
pbeIterationCount)
+ if err != nil {
+ return nil, errors.New("pkcs12: error encoding PKCS#8 shrouded key bag
when encrypting cert bag: " + err.Error())
+ }
+
+ params, err := getAlgorithmParams(salt, pbeIterationCount)
+ if err != nil {
+ return nil, errors.New("pkcs12: error encoding PKCS#8 shrouded key bag
algorithm's parameters: " + err.Error())
+ }
+
+ pkinfo := encryptedPrivateKeyInfo{
+ AlgorithmIdentifier: pkix.AlgorithmIdentifier{
+ Algorithm: oidPBEWithSHAAnd3KeyTripleDESCBC,
+ Parameters: params,
+ },
+ EncryptedData: pkData,
+ }
+
+ bytes, err = asn1.Marshal(pkinfo)
+ if err != nil {
+ return nil, errors.New("pkcs12: error encoding PKCS#8 shrouded key
bag: " + err.Error())
+ }
+
+ return bytes, err
+}
+
func decodePkcs8ShroudedKeyBag(asn1Data, password []byte) (privateKey
interface{}, err error) {
pkinfo := new(encryptedPrivateKeyInfo)
if err = unmarshal(asn1Data, pkinfo); err != nil {
diff --git a/pkcs12/safebags_test.go b/pkcs12/safebags_test.go
new file mode 100644
index 0000000..07b5fef
--- /dev/null
+++ b/pkcs12/safebags_test.go
@@ -0,0 +1,75 @@
+package pkcs12
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/asn1"
+ "testing"
+)
+
+// Assert the default algorithm parameters are in the correct order,
+// and default to the correct value. Defaults are based on OpenSSL.
+// 1. IterationCount, defaults to 2,048 long.
+// 2. Salt, is 8 bytes long.
+func TestDefaultAlgorithmParametersPkcs8ShroudedKeyBag(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ password := []byte("sesame")
+ bytes, err := encodePkcs8ShroudedKeyBag(privateKey, password)
+ if err != nil {
+ t.Fatalf("failed to encode PKCS#8 shrouded key bag: %s", err)
+ }
+
+ var pkinfo encryptedPrivateKeyInfo
+ rest, err := asn1.Unmarshal(bytes, &pkinfo)
+ if err != nil {
+ t.Fatalf("failed to unmarshal encryptedPrivateKeyInfo %s", err)
+ }
+
+ if len(rest) != 0 {
+ t.Fatalf("unexpected trailing bytes of len=%d, bytes=%x", len(rest),
rest)
+ }
+
+ var params pbeParams
+ rest, err = asn1.Unmarshal(pkinfo.Algorithm().Parameters.FullBytes,
¶ms)
+ if err != nil {
+ t.Fatalf("failed to unmarshal encryptedPrivateKeyInfo %s", err)
+ }
+
+ if len(rest) != 0 {
+ t.Fatalf("unexpected trailing bytes of len=%d, bytes=%x", len(rest),
rest)
+ }
+
+ if params.Iterations != pbeIterationCount {
+ t.Errorf("expected iteration count to be %d, but actual=%d",
pbeIterationCount, params.Iterations)
+ }
+ if len(params.Salt) != pbeSaltSizeBytes {
+ t.Errorf("expected the number of salt bytes to be %d, but actual=%d",
pbeSaltSizeBytes, len(params.Salt))
+ }
+}
+
+func TestRoundTripPkcs8ShroudedKeyBag(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ password := []byte("sesame")
+ bytes, err := encodePkcs8ShroudedKeyBag(privateKey, password)
+ if err != nil {
+ t.Fatalf("failed to encode PKCS#8 shrouded key bag: %s", err)
+ }
+
+ key, err := decodePkcs8ShroudedKeyBag(bytes, password)
+ if err != nil {
+ t.Fatalf("failed to decode PKCS#8 shrouded key bag: %s", err)
+ }
+
+ actualPrivateKey := key.(*rsa.PrivateKey)
+ if actualPrivateKey.D.Cmp(privateKey.D) != 0 {
+ t.Fatalf("failed to round-trip rsa.PrivateKey.D")
+ }
+}
--
https://go-review.googlesource.com/20075