I want to be able to send iOS APNS push notifications using AWS SNS with the aws golang SDK. I've created a p12 file following this instructions: https://support-aws.s3.amazonaws.com/Exporting-APNS-Cert-Keychain-Mac.pdf now in order to get the private key and cert I need to implement the following openssl equivalent commands:
openssl pkcs12 -in MyCertificates.p12 -out MyCer.pem -clcerts -nokeys
openssl pkcs12 -in MyCertificates.p12 -out MyKey.pem -nocerts -nodes
openssl pkcs8 -topk8 -inform pem -in MyKey.pem -outform pem -nocrypt -out MyKeyCorrectFormat.pem
I can't find a way to do this in golang, any help will be appreciated. What seems to be the issue is converting the private key to pkcs8 format.
EDIT:
This is what I have been trying to do (in order to compile you need to change the first import in github.com/youmark/pkcs8 to golang.org/x/crypto/pbkdf2) :
import (
"golang.org/x/crypto/pkcs12"
"io/ioutil"
"fmt"
"encoding/pem"
"github.com/aws/aws-sdk-go/service/sns"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/aws"
"crypto/x509"
"crypto/rsa"
"errors"
"github.com/youmark/pkcs8"
)
func main() {
b, err:= ioutil.ReadFile("myP12File.p12")
if err!=nil {
fmt.Println(err)
return
}
password := "123456"
_, pKey , err := Decode(b,password)
pKeyPkcs8, err := pkcs8.ConvertPrivateKeyToPKCS8(pKey,passwordBytes)
if err!=nil {
fmt.Println(err)
}
fmt.Println(string(pKeyPkcs8))
}
// Decode and verify an in memory .p12 certificate (DER binary format).
func Decode(p12 []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
// decode an x509.Certificate to verify
privateKey, cert, err := pkcs12.Decode(p12, password)
if err != nil {
return nil, nil, err
}
if err := verify(cert); err != nil {
return nil, nil, err
}
// assert that private key is RSA
priv, ok := privateKey.(*rsa.PrivateKey)
if !ok {
return nil, nil, errors.New("expected RSA private key type")
}
return cert, priv, nil
}
// verify checks if a certificate has expired
func verify(cert *x509.Certificate) error {
_, err := cert.Verify(x509.VerifyOptions{})
if err == nil {
return nil
}
switch e := err.(type) {
case x509.CertificateInvalidError:
switch e.Reason {
case x509.Expired:
return ErrExpired
default:
return err
}
case x509.UnknownAuthorityError:
// Apple cert isn't in the cert pool
// ignoring this error
return nil
default:
return err
}
}
// Certificate errors
var (
ErrExpired = errors.New("certificate has expired or is not yet valid")
)
What I get when printing the converted key is gibberish, so I guess there is something wrong with my decoding process somewhere.
I think you're there. You've converted the key to PKCS#8 format, but it's displaying as gibberish because it's printed in binary DER form. The key just needs to be encoded in pem format.
One way to test this is by creating your own pkcs#12 file containing a self signed certificate & key. A benefit is you can vary the expiry duration to exercise your certificate expiry error handling:
go run generate_cert.go -ca -duration 30m -host gooble.com
It generates key.pem and cert.pem. Combine key & cert:
cat key.pem cert.pem > both.pem
Bundle into pkcs#12:
openssl pkcs12 -export -in both.pem -out bundle.12 -nodes -password pass:123456
Here bundle.12 is a pkcs#12 file in binary DER form containing a certificate and private key, protected by a password.
Run the go program (see source below) to extract the certificate and key:
go run pk.go -in bundle.12 -outkey key8.pem -outcert outcert.pem -password 123456
The extracted certificate is identical to the original certificate. The extracted private key is similar to the original private key, but now in pkcs#8 format.
You can extract the original rsa key from the pkcs8 file. The original key.pem is identical to key.final.pem:
openssl rsa -in key8.pem -out key.final.pem
You could also verify the extracted pkcs#8 private key has the same modulus as the original certificate:
openssl x509 -in cert.pem -noout -modulus
Modulus=AEB5770C4DA8D...05E12398BE1
openssl rsa -in key8.pem -noout -modulus
Modulus=AEB5770C4DA8D...05E12398BE1
Note that the extracted pkcs#8 private key is unencrypted; that may not be what you want depending on how the key's going to be used.
Here's a slightly modified version of the go program (pk.go):
package main
import (
"crypto/x509"
"encoding/pem"
"errors"
"flag"
"github.com/youmark/pkcs8"
"golang.org/x/crypto/pkcs12"
"io/ioutil"
"log"
"os"
)
var (
in = flag.String("in", "", "pkcs#12 input file (private key and certificate only)")
password = flag.String("password", "", "to unlock the pkcs#12 bundle")
outkey = flag.String("outkey", "", "output filename of private key in pkcs#8 PEM format")
outcert = flag.String("outcert", "", "output filename of certificate in PEM format")
)
func main() {
flag.Parse()
if *in == "" || *password == "" || *outkey == "" || *outcert == "" {
flag.Usage()
os.Exit(1)
}
data, err := ioutil.ReadFile(*in)
if err != nil {
log.Fatal(err)
}
privateKey, certificate, err := pkcs12.Decode(data, *password)
if err != nil {
log.Fatal(err)
}
if err := verify(certificate); err != nil {
log.Fatal(err)
}
keyBytes, err := pkcs8.ConvertPrivateKeyToPKCS8(privateKey)
if err != nil {
log.Fatal(err)
}
//could write private key as binary DER encoded (instead of pem below)
//_, err = ioutil.WriteFile(*outkey,keyBytes,0644)
//write private key as pem
keyFile, err := os.Create(*outkey)
if err != nil {
log.Fatal(err)
}
defer keyFile.Close()
err = pem.Encode(keyFile, &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
if err != nil {
log.Fatal(err)
}
certFile, err := os.Create(*outcert)
if err != nil {
log.Fatal(err)
}
defer certFile.Close()
err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw})
if err != nil {
log.Fatal(err)
}
}
// verify checks if a certificate has expired
func verify(cert *x509.Certificate) error {
_, err := cert.Verify(x509.VerifyOptions{})
if err == nil {
return nil
}
switch e := err.(type) {
case x509.CertificateInvalidError:
switch e.Reason {
case x509.Expired:
return ErrExpired
default:
return err
}
case x509.UnknownAuthorityError:
// Apple cert isn't in the cert pool
// ignoring this error
return nil
default:
return err
}
}
// Certificate errors
var (
ErrExpired = errors.New("certificate has expired or is not yet valid")
)
Hope that helps.