Skip to content

Commit

Permalink
Add atca-gen-cert - command to generate ATCA key and cert in one go
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Deomid Ryabkov authored and cesantabot committed Mar 16, 2019
1 parent e4ba5c1 commit d896418
Show file tree
Hide file tree
Showing 8 changed files with 545 additions and 127 deletions.
334 changes: 209 additions & 125 deletions mos/atca.go

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions 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
}
10 changes: 9 additions & 1 deletion mos/flags/flags.go
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion mos/flagutils.go
Expand Up @@ -30,7 +30,6 @@ var (
)

func initFlags() {
initATCAFlags()
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
hideFlags()
flag.Usage = usage
Expand Down
2 changes: 2 additions & 0 deletions mos/main.go
Expand Up @@ -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},
Expand Down
189 changes: 189 additions & 0 deletions 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
}
15 changes: 15 additions & 0 deletions mos/x509utils/gen_cert.go
Expand Up @@ -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 {
Expand Down
45 changes: 45 additions & 0 deletions 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)
}

0 comments on commit d896418

Please sign in to comment.