From d8964189f37f2fb3fd32816b432888bcb662c14d Mon Sep 17 00:00:00 2001 From: Deomid Ryabkov Date: Sat, 16 Mar 2019 18:59:10 +0000 Subject: [PATCH] Add atca-gen-cert - command to generate ATCA key and cert in one go A command to generate ATCA key and cert in one go: ``` $ mos atca-gen-cert 1 cert1.crt.pem --subject=CN="OMG WTF" --cert-days=3650 --ca-cert-file ~/ca-box/data/ca.crt --ca-key-file ~/ca-box/data/ca.key --dry-run=false Using port /dev/ttyUSB0 Signing certificate: Using existing cert: /home/rojer/ca-box/data/ca.crt Using existing key : /home/rojer/ca-box/data/ca.key (ECDSA) ATECC608A rev 0x6002 S/N 0x0123f196b09f52ceee, config is locked, data is locked Generated new ECC key on slot 1 Subject: CN=OMG WTF Writing certificate to cert1.crt.pem... Uploading cert1.crt.pem (407 bytes)... $ ``` This generates new private key in slot 1, issues a certificate (signed by ca.crt/ca.key) and uploads it to the device. Shameless plug: https://github.com/rojer/ca-box for a simple "CA in a box" CL: mos: Add atca-gen-cert - command to generate ATCA key and cert in one go PUBLISHED_FROM=cba91fe4bd34938122fb124ab93948110d7c5a9d --- mos/atca.go | 334 ++++++++++++++++++++++++-------------- mos/atca/key_utils.go | 76 +++++++++ mos/flags/flags.go | 10 +- mos/flagutils.go | 1 - mos/main.go | 2 + mos/x509utils/dn.go | 189 +++++++++++++++++++++ mos/x509utils/gen_cert.go | 15 ++ mos/x509utils/utils.go | 45 +++++ 8 files changed, 545 insertions(+), 127 deletions(-) create mode 100644 mos/atca/key_utils.go create mode 100644 mos/x509utils/dn.go create mode 100644 mos/x509utils/utils.go diff --git a/mos/atca.go b/mos/atca.go index bfab457..ae84a82 100644 --- a/mos/atca.go +++ b/mos/atca.go @@ -1,40 +1,34 @@ package main import ( - "crypto/ecdsa" - "crypto/elliptic" + "context" + "crypto" + "crypto/rand" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" - "io" + "fmt" "io/ioutil" "math/big" "os" "strconv" "strings" - - "context" + "time" "cesanta.com/mos/atca" "cesanta.com/mos/dev" + "cesanta.com/mos/flags" + "cesanta.com/mos/x509utils" "github.com/cesanta/errors" flag "github.com/spf13/pflag" yaml "gopkg.in/yaml.v2" ) var ( - format string - writeKey string csrTemplate string ) -func initATCAFlags() { - flag.StringVar(&format, "format", "", "Config format, hex or json") - flag.StringVar(&writeKey, "write-key", "", "Write key file") - flag.StringVar(&csrTemplate, "csr-template", "", "CSR template to use") -} - func getFormat(f, fn string) string { f = strings.ToLower(f) if f == "" { @@ -62,7 +56,7 @@ func atcaGetConfig(ctx context.Context, dc dev.DevConn) error { return errors.Annotatef(err, "Connect") } - f := getFormat(format, fn) + f := getFormat(*flags.Format, fn) var s []byte if f == "json" || f == "yaml" { @@ -101,7 +95,7 @@ func atcaSetConfig(ctx context.Context, dc dev.DevConn) error { return errors.Trace(err) } - f := getFormat(format, fn) + f := getFormat(*flags.Format, fn) var confData []byte if f == "yaml" || f == "json" { @@ -245,20 +239,20 @@ func atcaSetECCPrivateKey(slot int64, cfg *atca.Config, data []byte) (*atca.SetK "are not enabled, key cannot be set", slot) } wks := int64(cfg.SlotInfo[slot].SlotConfig.WriteKey) - if writeKey == "" { + if *flags.WriteKey == "" { return nil, errors.Errorf( "data zone is locked, --write-key for slot %d "+ "is required to modify slot %d", wks, slot) } reportf("Data zone is locked, "+ - "will perform encrypted write using slot %d using %s", wks, writeKey) - wKeyData, err := ioutil.ReadFile(writeKey) + "will perform encrypted write using slot %d using %s", wks, *flags.WriteKey) + wKeyData, err := ioutil.ReadFile(*flags.WriteKey) if err != nil { return nil, errors.Trace(err) } wKey := atca.ReadHex(wKeyData) if len(wKey) != atca.KeySize { - return nil, errors.Errorf("%s: expected %d bytes, got %d", writeKey, atca.KeySize, len(wKey)) + return nil, errors.Errorf("%s: expected %d bytes, got %d", *flags.WriteKey, atca.KeySize, len(wKey)) } b64wk := base64.StdEncoding.EncodeToString(wKey) req.Wkslot = &wks @@ -335,86 +329,92 @@ func atcaSetKey(ctx context.Context, dc dev.DevConn) error { return nil } -func writePEM(derBytes []byte, blockType string, outputFileName string) error { - var out io.Writer - switch outputFileName { - case "": - out = os.Stdout - case "-": - out = os.Stdout - case "--": - out = os.Stderr - default: - f, err := os.OpenFile(outputFileName, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - return errors.Annotatef(err, "failed to open %s for writing", outputFileName) - } - out = f - defer func() { - f.Close() - reportf("Wrote %s", outputFileName) - }() +func atcaGenKey(ctx context.Context, dc dev.DevConn) error { + args := flag.Args() + if len(args) < 2 { + return errors.Errorf("slot number is required") + } + slot, err := strconv.ParseInt(args[1], 0, 64) + if err != nil || slot < 0 || slot > 15 { + return errors.Errorf("invalid slot number %q", args[1]) + } + + outputFileName := "" + if len(args) == 3 { + outputFileName = args[2] } - pem.Encode(out, &pem.Block{Type: blockType, Bytes: derBytes}) - return nil -} -func genCSR(csrTemplateFile string, slot int, dc dev.DevConn, outputFileName string) error { - reportf("Generating CSR using template from %s", csrTemplateFile) - data, err := ioutil.ReadFile(csrTemplateFile) + if _, _, err := atca.Connect(ctx, dc); err != nil { + return errors.Annotatef(err, "Connect") + } + pubKeyData, err := atca.GenKey(ctx, int(slot), *dryRun, dc) if err != nil { return errors.Trace(err) } + if pubKeyData == nil { // dry run + return nil + } - var pb *pem.Block - pb, _ = pem.Decode(data) - if pb == nil { - return errors.Errorf("%s: not a PEM file", csrTemplateFile) + return x509utils.WritePubKey(pubKeyData, outputFileName) +} + +func genCSR(ctx context.Context, csrTemplateFile string, subject string, slot int, dc dev.DevConn, outputFileName string) ([]byte, error) { + if csrTemplateFile == "" && subject == "" { + return nil, errors.Errorf("CSR template file or subject is required") } - if pb.Type != "CERTIFICATE REQUEST" { - return errors.Errorf("%s: expected to find certificate, found %s", csrTemplateFile, pb.Type) + var csrTemplate *x509.CertificateRequest + if csrTemplateFile != "" { + reportf("Generating CSR using template from %s", csrTemplateFile) + data, err := ioutil.ReadFile(csrTemplateFile) + if err != nil { + return nil, errors.Trace(err) + } + + var pb *pem.Block + pb, _ = pem.Decode(data) + if pb == nil { + return nil, errors.Errorf("%s: not a PEM file", csrTemplateFile) + } + if pb.Type != "CERTIFICATE REQUEST" { + return nil, errors.Errorf("%s: expected to find certificate request, found %s", csrTemplateFile, pb.Type) + } + csrTemplate, err = x509.ParseCertificateRequest(pb.Bytes) + if err != nil { + return nil, errors.Annotatef(err, "%s: failed to parse certificate request template", csrTemplateFile) + } + } else { + // Create a simple CSR. + csrTemplate = &x509.CertificateRequest{ + PublicKeyAlgorithm: x509.ECDSA, + SignatureAlgorithm: x509.ECDSAWithSHA256, + } } - csrTemplate, err := x509.ParseCertificateRequest(pb.Bytes) - if err != nil { - return errors.Annotatef(err, "%s: failed to parse certificate", csrTemplateFile) + if subject != "" { + subj, err := x509utils.ParseDN(subject) + if err != nil { + return nil, errors.Annotatef(err, "invalid subject %q", subject) + } + csrTemplate.Subject = *subj } - reportf("%s: public key type %d, signature type %d\n%s", - csrTemplateFile, csrTemplate.PublicKeyAlgorithm, csrTemplate.SignatureAlgorithm, - csrTemplate.Subject.ToRDNSequence()) + reportf("Subject: %s", csrTemplate.Subject.ToRDNSequence()) if csrTemplate.PublicKeyAlgorithm != x509.ECDSA || csrTemplate.SignatureAlgorithm != x509.ECDSAWithSHA256 { - return errors.Errorf("%s: wrong public key and/or signature type; "+ + return nil, errors.Errorf("%s: wrong public key and/or signature type; "+ "expected ECDSA(%d) and SHA256(%d), got %d %d", csrTemplateFile, x509.ECDSA, x509.ECDSAWithSHA256, csrTemplate.PublicKeyAlgorithm, csrTemplate.SignatureAlgorithm) } - signer := atca.NewSigner(context.Background(), dc, slot) - csr, err := x509.CreateCertificateRequest(nil, csrTemplate, signer) + signer := atca.NewSigner(ctx, dc, slot) + csrData, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, signer) if err != nil { - return errors.Annotatef(err, "failed to create new CSR") + return nil, errors.Annotatef(err, "failed to create new CSR") } - return writePEM(csr, "CERTIFICATE REQUEST", outputFileName) + return csrData, nil } -func writePubKey(pubKeyData []byte, outputFileName string) error { - if len(pubKeyData) != 64 { - return errors.Errorf("expected 64 bytes of public key data, got %d", len(pubKeyData)) - } - pubKey := &ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: big.NewInt(0).SetBytes(pubKeyData[0:32]), - Y: big.NewInt(0).SetBytes(pubKeyData[32:64]), - } - pubKeyDERBytes, err := x509.MarshalPKIXPublicKey(pubKey) - if err != nil { - return errors.Annotatef(err, "failed to marshal public key") - } - return writePEM(pubKeyDERBytes, "PUBLIC KEY", outputFileName) -} - -func atcaGenKey(ctx context.Context, dc dev.DevConn) error { +func atcaGenCSR(ctx context.Context, dc dev.DevConn) error { args := flag.Args() if len(args) < 2 { return errors.Errorf("slot number is required") @@ -423,53 +423,109 @@ func atcaGenKey(ctx context.Context, dc dev.DevConn) error { if err != nil || slot < 0 || slot > 15 { return errors.Errorf("invalid slot number %q", args[1]) } - outputFileName := "" if len(args) == 3 { outputFileName = args[2] } + if *flags.CSRTemplate == "" && *flags.Subject == "" { + return errors.Errorf("--csr-template or --subject is required") + } - if _, _, err = atca.Connect(ctx, dc); err != nil { + if _, _, err := atca.Connect(ctx, dc); err != nil { return errors.Annotatef(err, "Connect") } - - req := &atca.GenKeyArgs{Slot: &slot} - - if *dryRun { - reportf("This is a dry run, would have sent the following request:\n\n"+ - "GenKey %s\n\n"+ - "Set --dry-run=false to confirm.", - atca.JSONStr(*req)) + pubKeyData, err := atca.GenKey(ctx, int(slot), *dryRun, dc) + if err != nil { + return errors.Trace(err) + } + if pubKeyData == nil { // dry run return nil } - var r atca.GenKeyResult - if err := dc.Call(ctx, "ATCA.GenKey", req, &r); err != nil { - return errors.Annotatef(err, "GenKey") + csrData, err := genCSR(ctx, *flags.CSRTemplate, *flags.Subject, int(slot), dc, outputFileName) + if err != nil { + return errors.Annotatef(err, "genCSR") } - if r.Pubkey == nil { - return errors.New("no public key in response") - } + return x509utils.WritePEM(csrData, "CERTIFICATE REQUEST", outputFileName) +} - keyData, err := base64.StdEncoding.DecodeString(*r.Pubkey) - if err != nil { - return errors.Annotatef(err, "failed to decode pub key data") +func genCert(ctx context.Context, certTemplateFile string, subject string, validityDays int, slot int, caCert *x509.Certificate, caSigner crypto.Signer, dc dev.DevConn, outputFileName string) ([]byte, error) { + if certTemplateFile == "" && subject == "" { + return nil, errors.Errorf("cert template file or subject is required") } - if len(keyData) != atca.PublicKeySize { - return errors.Errorf("expected %d bytes, got %d", atca.PublicKeySize, len(keyData)) + if !caCert.IsCA { + return nil, errors.Errorf("signing cert is not a CA cert") } - - reportf("Generated new ECC key on slot %d", slot) - - if csrTemplate != "" { - return genCSR(csrTemplate, int(slot), dc, outputFileName) + var certTemplate *x509.Certificate + if certTemplateFile != "" { + reportf("Generating cert using template from %s", certTemplateFile) + data, err := ioutil.ReadFile(certTemplateFile) + if err != nil { + return nil, errors.Trace(err) + } + var pb *pem.Block + pb, _ = pem.Decode(data) + if pb == nil { + return nil, errors.Errorf("%s: not a PEM file", certTemplateFile) + } + if pb.Type != "CERTIFICATE" { + return nil, errors.Errorf("%s: expected to find certificate, found %s", certTemplateFile, pb.Type) + } + certTemplate, err = x509.ParseCertificate(pb.Bytes) + if err != nil { + return nil, errors.Annotatef(err, "%s: failed to parse certificate template", certTemplateFile) + } } else { - return writePubKey(keyData, outputFileName) + // Create a simple cert. + sn, _ := rand.Int(rand.Reader, big.NewInt(1<<63-1)) + certTemplate = &x509.Certificate{ + SerialNumber: sn, + PublicKeyAlgorithm: x509.ECDSA, + SignatureAlgorithm: x509.ECDSAWithSHA256, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth, + }, + IsCA: false, + BasicConstraintsValid: true, + } } + if subject != "" { + subj, err := x509utils.ParseDN(subject) + if err != nil { + return nil, errors.Annotatef(err, "invalid subject %q", subject) + } + certTemplate.Subject = *subj + } + if validityDays > 0 { + certTemplate.NotBefore = time.Now() + certTemplate.NotAfter = time.Now().Add(time.Duration(validityDays*24) * time.Hour) + } + if time.Now().After(certTemplate.NotAfter) { + return nil, errors.Errorf("invalid certificate validity, must be provided by template or --cert-days") + } + reportf("Subject: %s", certTemplate.Subject.ToRDNSequence()) + if certTemplate.PublicKeyAlgorithm != x509.ECDSA || + certTemplate.SignatureAlgorithm != x509.ECDSAWithSHA256 { + return nil, errors.Errorf("%s: wrong public key and/or signature type; "+ + "expected ECDSA(%d) and SHA256(%d), got %d %d", + certTemplateFile, + x509.ECDSA, x509.ECDSAWithSHA256, certTemplate.PublicKeyAlgorithm, + certTemplate.SignatureAlgorithm) + } + pubKey, err := atca.GetPubKey(ctx, slot, dc) + if err != nil { + return nil, errors.Annotatef(err, "GetPubKey") + } + certData, err := x509.CreateCertificate(rand.Reader, certTemplate, caCert, pubKey, caSigner) + if err != nil { + return nil, errors.Annotatef(err, "failed to create new certificate") + } + return certData, nil } -func atcaGetPubKey(ctx context.Context, dc dev.DevConn) error { +func atcaGenCert(ctx context.Context, dc dev.DevConn) error { args := flag.Args() if len(args) < 2 { return errors.Errorf("slot number is required") @@ -478,38 +534,66 @@ func atcaGetPubKey(ctx context.Context, dc dev.DevConn) error { if err != nil || slot < 0 || slot > 15 { return errors.Errorf("invalid slot number %q", args[1]) } - + if *flags.CAFile == "" && *flags.CAKeyFile == "" { + return errors.Errorf("--ca-cert-file and --ca-key-file are required") + } outputFileName := "" if len(args) == 3 { outputFileName = args[2] } + if *flags.CertTemplate == "" && *flags.Subject == "" { + return errors.Errorf("--cert-template or --subject is required") + } + reportf("Signing certificate:") + caCertDERBytes, _, caSigner, _, _, err := x509utils.LoadCertAndKey(*flags.CAFile, *flags.CAKeyFile) + if err != nil { + return errors.Annotatef(err, "failed to load signing cert") + } + caCert, err := x509.ParseCertificate(caCertDERBytes) + if err != nil { + return errors.Annotatef(err, "invalid signing cert") + } + if _, _, err := atca.Connect(ctx, dc); err != nil { return errors.Annotatef(err, "Connect") } - - req := &atca.GetPubKeyArgs{Slot: &slot} - - var r atca.GetPubKeyResult - if err := dc.Call(ctx, "ATCA.GetPubKey", req, nil); err != nil { - return errors.Annotatef(err, "GetPubKey") + pubKey, err := atca.GenKey(ctx, int(slot), *dryRun, dc) + if err != nil { + return errors.Trace(err) } - - if r.Pubkey == nil { - return errors.New("no public key in response") + if pubKey == nil { // dry run + return nil } - keyData, err := base64.StdEncoding.DecodeString(*r.Pubkey) + certData, err := genCert(ctx, *flags.CertTemplate, *flags.Subject, *flags.CertDays, int(slot), caCert, caSigner, dc, outputFileName) if err != nil { - return errors.Annotatef(err, "failed to decode pub key data") - } - if len(keyData) != atca.PublicKeySize { - return errors.Errorf("expected %d bytes, got %d", atca.PublicKeySize, len(keyData)) + return errors.Annotatef(err, "genCert") } - if csrTemplate != "" { - return genCSR(csrTemplate, int(slot), dc, outputFileName) - } else { - return writePubKey(keyData, outputFileName) + _, err = x509utils.WriteAndUploadFile(ctx, "certificate", certData, outputFileName, fmt.Sprintf("atca-%d.crt.pem", slot), dc) + return err +} + +func atcaGetPubKey(ctx context.Context, dc dev.DevConn) error { + args := flag.Args() + if len(args) < 2 { + return errors.Errorf("slot number is required") + } + slot, err := strconv.ParseInt(args[1], 0, 64) + if err != nil || slot < 0 || slot > 15 { + return errors.Errorf("invalid slot number %q", args[1]) + } + outputFileName := "" + if len(args) == 3 { + outputFileName = args[2] + } + if _, _, err := atca.Connect(ctx, dc); err != nil { + return errors.Annotatef(err, "Connect") + } + pubKey, err := atca.GetPubKey(ctx, int(slot), dc) + if err != nil { + return errors.Annotatef(err, "getPubKey") } + return x509utils.WritePubKey(pubKey, outputFileName) } diff --git a/mos/atca/key_utils.go b/mos/atca/key_utils.go new file mode 100644 index 0000000..7ea0ec9 --- /dev/null +++ b/mos/atca/key_utils.go @@ -0,0 +1,76 @@ +package atca + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "encoding/base64" + "math/big" + + "github.com/cesanta/errors" + + "cesanta.com/common/go/ourutil" + "cesanta.com/mos/dev" +) + +func GetPubKey(ctx context.Context, slot int, dc dev.DevConn) (*ecdsa.PublicKey, error) { + slot64 := int64(slot) + req := &GetPubKeyArgs{Slot: &slot64} + var r GetPubKeyResult + if err := dc.Call(ctx, "ATCA.GetPubKey", req, &r); err != nil { + return nil, errors.Annotatef(err, "GetPubKey") + } + if r.Pubkey == nil { + return nil, errors.New("no public key in response") + } + pubKeyData, err := base64.StdEncoding.DecodeString(*r.Pubkey) + if err != nil { + return nil, errors.Annotatef(err, "failed to decode pub key data") + } + if len(pubKeyData) != PublicKeySize { + return nil, errors.Errorf("expected %d bytes, got %d", PublicKeySize, len(pubKeyData)) + } + pubKey := &ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: big.NewInt(0).SetBytes(pubKeyData[0:32]), + Y: big.NewInt(0).SetBytes(pubKeyData[32:64]), + } + return pubKey, nil +} + +func GenKey(ctx context.Context, slot int, dryRun bool, dc dev.DevConn) (*ecdsa.PublicKey, error) { + slot64 := int64(slot) + req := &GenKeyArgs{Slot: &slot64} + + if dryRun { + ourutil.Reportf("This is a dry run, would have sent the following request:\n\n"+ + "GenKey %s\n\n"+ + "Set --dry-run=false to confirm.", + JSONStr(*req)) + return nil, nil + } + + var r GenKeyResult + if err := dc.Call(ctx, "ATCA.GenKey", req, &r); err != nil { + return nil, errors.Annotatef(err, "GenKey") + } + + if r.Pubkey == nil { + return nil, errors.New("no public key in response") + } + + pubKeyData, err := base64.StdEncoding.DecodeString(*r.Pubkey) + if err != nil { + return nil, errors.Annotatef(err, "failed to decode pub key data") + } + if len(pubKeyData) != PublicKeySize { + return nil, errors.Errorf("expected %d bytes, got %d", PublicKeySize, len(pubKeyData)) + } + ourutil.Reportf("Generated new ECC key on slot %d", slot) + pubKey := &ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: big.NewInt(0).SetBytes(pubKeyData[0:32]), + Y: big.NewInt(0).SetBytes(pubKeyData[32:64]), + } + return pubKey, nil +} diff --git a/mos/flags/flags.go b/mos/flags/flags.go index 65779ce..9cb18ee 100644 --- a/mos/flags/flags.go +++ b/mos/flags/flags.go @@ -35,7 +35,8 @@ var ( UID = flag.String("uid", "", "") CertFile = flag.String("cert-file", "", "Certificate file name") KeyFile = flag.String("key-file", "", "Key file name") - CAFile = flag.String("ca-cert-file", "", "CA cert for TLS server verification") + CAFile = flag.String("ca-cert-file", "", "CA certificate file name") + CAKeyFile = flag.String("ca-key-file", "", "CA key file name (for cert signing)") RPCUARTNoDelay = flag.Bool("rpc-uart-no-delay", false, "Do not introduce delay into UART over RPC") Timeout = flag.Duration("timeout", 20*time.Second, "Timeout for the device connection and call operation") Reconnect = flag.Bool("reconnect", false, "Enable reconnection") @@ -60,6 +61,13 @@ var ( NoReboot = flag.Bool("no-reboot", false, "Save config but don't reboot the device.") NoSave = flag.Bool("no-save", false, "Don't save config and don't reboot the device") TryOnce = flag.Bool("try-once", false, "When saving the config, do it in such a way that it's only applied on the next boot") + + Format = flag.String("format", "", "Config format, hex or json") + WriteKey = flag.String("write-key", "", "Write key file") + CSRTemplate = flag.String("csr-template", "", "CSR template to use") + CertTemplate = flag.String("cert-template", "", "cert template to use") + CertDays = flag.Int("cert-days", 0, "new cert validity, days") + Subject = flag.String("subject", "", "Subject for CSR or certificate") ) func Platform() string { diff --git a/mos/flagutils.go b/mos/flagutils.go index 296f192..4b68f3c 100644 --- a/mos/flagutils.go +++ b/mos/flagutils.go @@ -30,7 +30,6 @@ var ( ) func initFlags() { - initATCAFlags() flag.CommandLine.AddGoFlagSet(goflag.CommandLine) hideFlags() flag.Usage = usage diff --git a/mos/main.go b/mos/main.go index c711d26..ee56320 100644 --- a/mos/main.go +++ b/mos/main.go @@ -135,6 +135,8 @@ func init() { {"atca-set-key", atcaSetKey, `Set key in a given slot`, nil, []string{"dry-run", "port", "write-key"}, Yes, true}, {"atca-gen-key", atcaGenKey, `Generate a random key in a given slot`, nil, []string{"dry-run", "port"}, Yes, true}, {"atca-get-pub-key", atcaGetPubKey, `Retrieve public ECC key from a given slot`, nil, []string{"port"}, Yes, true}, + {"atca-gen-csr", atcaGenCSR, `Generate a random key in a given slot and generate a certificate request file`, nil, []string{"port"}, Yes, true}, + {"atca-gen-cert", atcaGenCert, `Generate a random key in a given slot and issue a certificate`, nil, []string{"port"}, Yes, true}, {"esp32-efuse-get", esp32EFuseGet, `Get ESP32 eFuses`, nil, nil, No, true}, {"esp32-efuse-set", esp32EFuseSet, `Set ESP32 eFuses`, nil, nil, No, true}, {"esp32-encrypt-image", esp32EncryptImage, `Encrypt a ESP32 firmware image`, []string{"esp32-encryption-key-file", "esp32-flash-address"}, nil, No, true}, diff --git a/mos/x509utils/dn.go b/mos/x509utils/dn.go new file mode 100644 index 0000000..e0da54b --- /dev/null +++ b/mos/x509utils/dn.go @@ -0,0 +1,189 @@ +package x509utils + +// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com) +// Portions copyright (c) 2015-2016 go-ldap Authors +// +// From https://github.com/go-ldap/ldap/blob/master/dn.go +// Lisense: MIT + +import ( + "bytes" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/hex" + "fmt" + "strconv" + "strings" + + "github.com/cesanta/errors" +) + +var attributeTypeNames = map[string]asn1.ObjectIdentifier{ + "CN": asn1.ObjectIdentifier{2, 5, 4, 3}, + "SERIALNUMBER": asn1.ObjectIdentifier{2, 5, 4, 5}, + "C": asn1.ObjectIdentifier{2, 5, 4, 6}, + "L": asn1.ObjectIdentifier{2, 5, 4, 7}, + "ST": asn1.ObjectIdentifier{2, 5, 4, 8}, + "STREET": asn1.ObjectIdentifier{2, 5, 4, 9}, + "O": asn1.ObjectIdentifier{2, 5, 4, 10}, + "OU": asn1.ObjectIdentifier{2, 5, 4, 11}, + "POSTALCODE": asn1.ObjectIdentifier{2, 5, 4, 17}, +} + +// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514 +type AttributeTypeAndValue struct { + // Type is the attribute type + Type string + // Value is the attribute value + Value string +} + +// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514 +type RelativeDN struct { + Attributes []*AttributeTypeAndValue +} + +// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514 +type DN struct { + RDNs []*RelativeDN +} + +// ParseDN returns a distinguishedName or an error +func ParseDN(str string) (*pkix.Name, error) { + dn := new(DN) + dn.RDNs = make([]*RelativeDN, 0) + rdn := new(RelativeDN) + rdn.Attributes = make([]*AttributeTypeAndValue, 0) + buffer := bytes.Buffer{} + attribute := new(AttributeTypeAndValue) + escaping := false + + unescapedTrailingSpaces := 0 + stringFromBuffer := func() string { + s := buffer.String() + s = s[0 : len(s)-unescapedTrailingSpaces] + buffer.Reset() + unescapedTrailingSpaces = 0 + return s + } + + for i := 0; i < len(str); i++ { + char := str[i] + switch { + case escaping: + unescapedTrailingSpaces = 0 + escaping = false + switch char { + case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\': + buffer.WriteByte(char) + continue + } + // Not a special character, assume hex encoded octet + if len(str) == i+1 { + return nil, errors.New("got corrupted escaped character") + } + + dst := []byte{0} + n, err := hex.Decode([]byte(dst), []byte(str[i:i+2])) + if err != nil { + return nil, fmt.Errorf("failed to decode escaped character: %s", err) + } else if n != 1 { + return nil, fmt.Errorf("expected 1 byte when un-escaping, got %d", n) + } + buffer.WriteByte(dst[0]) + i++ + case char == '\\': + unescapedTrailingSpaces = 0 + escaping = true + case char == '=': + attribute.Type = stringFromBuffer() + // Special case: If the first character in the value is # the + // following data is BER encoded so we can just fast forward + // and decode. + if len(str) > i+1 && str[i+1] == '#' { + return nil, errors.New("BER values not supported") + /* + i += 2 + index := strings.IndexAny(str[i:], ",+") + data := str + if index > 0 { + data = str[i : i+index] + } else { + data = str[i:] + } + rawBER, err := enchex.DecodeString(data) + if err != nil { + return nil, fmt.Errorf("failed to decode BER encoding: %s", err) + } + packet, err := ber.DecodePacketErr(rawBER) + if err != nil { + return nil, fmt.Errorf("failed to decode BER packet: %s", err) + } + buffer.WriteString(packet.Data.String()) + i += len(data) - 1 + */ + } + case char == ',' || char == '+': + // We're done with this RDN or value, push it + if len(attribute.Type) == 0 { + return nil, errors.New("incomplete type, value pair") + } + attribute.Value = stringFromBuffer() + rdn.Attributes = append(rdn.Attributes, attribute) + attribute = new(AttributeTypeAndValue) + if char == ',' { + dn.RDNs = append(dn.RDNs, rdn) + rdn = new(RelativeDN) + rdn.Attributes = make([]*AttributeTypeAndValue, 0) + } + case char == ' ' && buffer.Len() == 0: + // ignore unescaped leading spaces + continue + default: + if char == ' ' { + // Track unescaped spaces in case they are trailing and we need to remove them + unescapedTrailingSpaces++ + } else { + // Reset if we see a non-space char + unescapedTrailingSpaces = 0 + } + buffer.WriteByte(char) + } + } + if buffer.Len() > 0 { + if len(attribute.Type) == 0 { + return nil, errors.New("DN ended with incomplete type, value pair") + } + attribute.Value = stringFromBuffer() + rdn.Attributes = append(rdn.Attributes, attribute) + dn.RDNs = append(dn.RDNs, rdn) + } + return dn.ToName() +} + +func (dn *DN) ToName() (*pkix.Name, error) { + var ns pkix.RDNSequence + for _, rdn := range dn.RDNs { + var s pkix.RelativeDistinguishedNameSET + for _, a := range rdn.Attributes { + oid, ok := attributeTypeNames[a.Type] + if !ok { + for _, p := range strings.Split(a.Type, ".") { + pi, err := strconv.Atoi(p) + if err != nil { + return nil, errors.Errorf("invalid attribute type %q", a.Type) + } + oid = append(oid, pi) + } + } + s = append(s, pkix.AttributeTypeAndValue{ + Type: oid, + Value: a.Value, + }) + } + ns = append(ns, s) + } + var n pkix.Name + n.FillFromRDNSequence(&ns) + return &n, nil +} diff --git a/mos/x509utils/gen_cert.go b/mos/x509utils/gen_cert.go index 304a015..04fbe6c 100644 --- a/mos/x509utils/gen_cert.go +++ b/mos/x509utils/gen_cert.go @@ -226,6 +226,21 @@ func LoadCertAndKey(certFile, keyFile string) ([]byte, []byte, crypto.Signer, [] if keySigner, err1 = x509.ParsePKCS1PrivateKey(kpb.Bytes); err1 != nil { return nil, nil, nil, nil, nil, errors.Annotatef(err1, "invalid RSA private key %s", keyFile) } + case "PRIVATE KEY": + k, err1 := x509.ParsePKCS8PrivateKey(kpb.Bytes) + if err1 != nil { + return nil, nil, nil, nil, nil, errors.Annotatef(err1, "invalid private key %s", keyFile) + } + switch k.(type) { + case *rsa.PrivateKey: + kt = "RSA" + keySigner = k.(*rsa.PrivateKey) + case *ecdsa.PrivateKey: + kt = "ECDSA" + keySigner = k.(*ecdsa.PrivateKey) + default: + return nil, nil, nil, nil, nil, errors.Errorf("unknown key type %T in %s", k, keyFile) + } case "EC PRIVATE KEY": kt = "ECDSA" if keySigner, err1 = x509.ParseECPrivateKey(kpb.Bytes); err1 != nil { diff --git a/mos/x509utils/utils.go b/mos/x509utils/utils.go new file mode 100644 index 0000000..e943fe5 --- /dev/null +++ b/mos/x509utils/utils.go @@ -0,0 +1,45 @@ +package x509utils + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "io" + "os" + + "github.com/cesanta/errors" + + "cesanta.com/common/go/ourutil" +) + +func WritePEM(derBytes []byte, blockType string, outputFileName string) error { + var out io.Writer + switch outputFileName { + case "": + out = os.Stdout + case "-": + out = os.Stdout + case "--": + out = os.Stderr + default: + f, err := os.OpenFile(outputFileName, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return errors.Annotatef(err, "failed to open %s for writing", outputFileName) + } + out = f + defer func() { + f.Close() + ourutil.Reportf("Wrote %s", outputFileName) + }() + } + pem.Encode(out, &pem.Block{Type: blockType, Bytes: derBytes}) + return nil +} + +func WritePubKey(pubKey *ecdsa.PublicKey, outputFileName string) error { + pubKeyDERBytes, err := x509.MarshalPKIXPublicKey(pubKey) + if err != nil { + return errors.Annotatef(err, "failed to marshal public key") + } + return WritePEM(pubKeyDERBytes, "PUBLIC KEY", outputFileName) +}