diff --git a/cmd/keypairs/keypairs.go b/cmd/keypairs/keypairs.go new file mode 100644 index 0000000..e61d22f --- /dev/null +++ b/cmd/keypairs/keypairs.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "strings" + + "git.rootprojects.org/root/keypairs" +) + +func main() { + if 1 == len(os.Args) || "gen" != os.Args[1] { + fmt.Fprintln(os.Stderr, "Usage: keypairs gen -o [--pub ]") + os.Exit(1) + return + } + + var privname string + var pubname string + flag.StringVar(&privname, "o", "", "private key file (should have .jwk.json or pkcs8.pem extension)") + flag.StringVar(&pubname, "pub", "", "public key file (should have .jwk.json or spki.pem extension)") + flag.Parse() + + priv := keypairs.NewDefaultPrivateKey() + marshalPriv(priv, privname) + marshalPub(keypairs.NewPublicKey(priv.Public()), pubname) +} + +func marshalPriv(priv keypairs.PrivateKey, privname string) { + if "" == privname { + b := indentJSON(keypairs.MarshalJWKPrivateKey(priv)) + + fmt.Fprintf(os.Stdout, string(b)+"\n") + return + } + + var b []byte + if strings.HasSuffix(privname, ".json") { + b = indentJSON(keypairs.MarshalJWKPrivateKey(priv)) + } else if strings.HasSuffix(privname, ".pem") { + b, _ = keypairs.MarshalPEMPrivateKey(priv) + } else if strings.HasSuffix(privname, ".der") { + b, _ = keypairs.MarshalDERPrivateKey(priv) + } else { + fmt.Fprintf(os.Stderr, "private key extension should be .jwk.json, .pem, or .der") + os.Exit(1) + return + } + + ioutil.WriteFile(privname, b, 0600) +} + +func marshalPub(pub keypairs.PublicKey, pubname string) { + var b []byte + if "" == pubname { + b = indentJSON(keypairs.MarshalJWKPublicKey(pub)) + + fmt.Fprintf(os.Stderr, string(b)+"\n") + return + } + + if strings.HasSuffix(pubname, ".json") { + b = indentJSON(keypairs.MarshalJWKPublicKey(pub)) + } else if strings.HasSuffix(pubname, ".pem") { + b, _ = keypairs.MarshalPEMPublicKey(pub) + } else if strings.HasSuffix(pubname, ".der") { + b, _ = keypairs.MarshalDERPublicKey(pub) + } + + ioutil.WriteFile(pubname, b, 0644) +} + +func indentJSON(b []byte) []byte { + m := map[string]interface{}{} + _ = json.Unmarshal(b, &m) + b, _ = json.MarshalIndent(&m, "", " ") + return b +} diff --git a/generate.go b/generate.go new file mode 100644 index 0000000..a3412a7 --- /dev/null +++ b/generate.go @@ -0,0 +1,83 @@ +package keypairs + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "io" + mathrand "math/rand" + "time" +) + +var randReader io.Reader = rand.Reader +var maxRetry = 1 + +// KeyOptions are the things that we may need to know about a request to fulfill it properly +type keyOptions struct { + //Key string `json:"key"` + KeyType string `json:"kty"` + //Seed int64 `json:"-"` + //SeedStr string `json:"seed"` + //Claims Object `json:"claims"` + //Header Object `json:"header"` +} + +// this shananigans is only for testing and debug API stuff +func (o *keyOptions) myFooNextReader() io.Reader { + return randReader + /* + if 0 == o.Seed { + return randReader + } + return mathrand.New(mathrand.NewSource(o.Seed)) + */ +} + +// NewDefaultPrivateKey generates a key with reasonable strength. +// Today that means a 256-bit equivalent - either RSA 2048 or EC P-256. +func NewDefaultPrivateKey() PrivateKey { + mathrand.Seed(time.Now().UnixNano()) + coin := mathrand.Int() + if 0 == coin%2 { + return newPrivateKey(&keyOptions{ + KeyType: "RSA", + }) + } + return newPrivateKey(&keyOptions{ + KeyType: "EC", + }) +} + +// newPrivateKey generates a 256-bit entropy RSA or ECDSA private key +func newPrivateKey(opts *keyOptions) PrivateKey { + var privkey PrivateKey + + if "RSA" == opts.KeyType { + keylen := 2048 + privkey, _ = rsa.GenerateKey(opts.myFooNextReader(), keylen) + /* + if 0 != opts.Seed { + for i := 0; i < maxRetry; i++ { + otherkey, _ := rsa.GenerateKey(opts.myFooNextReader(), keylen) + otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D) + if 0 != otherCmp { + // There are two possible keys, choose the lesser D value + // See https://github.com/square/go-jose/issues/189 + if otherCmp < 0 { + privkey = otherkey + } + break + } + if maxRetry == i-1 { + log.Printf("error: coinflip landed on heads %d times", maxRetry) + } + } + } + */ + } else { + // TODO: EC keys may also suffer the same random problems in the future + privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.myFooNextReader()) + } + return privkey +} diff --git a/marshal.go b/marshal.go new file mode 100644 index 0000000..2198c5e --- /dev/null +++ b/marshal.go @@ -0,0 +1,171 @@ +package keypairs + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "log" + "math/big" + mathrand "math/rand" +) + +// MarshalPEMPublicKey outputs the given public key as JWK +func MarshalPEMPublicKey(pubkey crypto.PublicKey) ([]byte, error) { + block, err := marshalDERPublicKey(pubkey) + if nil != err { + return nil, err + } + return pem.EncodeToMemory(block), nil +} + +// MarshalDERPublicKey outputs the given public key as JWK +func MarshalDERPublicKey(pubkey crypto.PublicKey) ([]byte, error) { + block, err := marshalDERPublicKey(pubkey) + if nil != err { + return nil, err + } + return block.Bytes, nil +} + +// marshalDERPublicKey outputs the given public key as JWK +func marshalDERPublicKey(pubkey crypto.PublicKey) (*pem.Block, error) { + + var der []byte + var typ string + var err error + switch k := pubkey.(type) { + case *rsa.PublicKey: + der = x509.MarshalPKCS1PublicKey(k) + typ = "RSA PUBLIC KEY" + case *ecdsa.PublicKey: + typ = "PUBLIC KEY" + der, err = x509.MarshalPKIXPublicKey(k) + if nil != err { + return nil, err + } + default: + panic("Developer Error: impossible key type") + } + + return &pem.Block{ + Bytes: der, + Type: typ, + }, nil +} + +// MarshalJWKPrivateKey outputs the given private key as JWK +func MarshalJWKPrivateKey(privkey PrivateKey) []byte { + // thumbprint keys are alphabetically sorted and only include the necessary public parts + switch k := privkey.(type) { + case *rsa.PrivateKey: + return MarshalRSAPrivateKey(k) + case *ecdsa.PrivateKey: + return MarshalECPrivateKey(k) + default: + // this is unreachable because we know the types that we pass in + log.Printf("keytype: %t, %+v\n", privkey, privkey) + panic(ErrInvalidPublicKey) + //return nil + } +} + +// MarshalDERPrivateKey outputs the given private key as ASN.1 DER +func MarshalDERPrivateKey(privkey PrivateKey) ([]byte, error) { + // thumbprint keys are alphabetically sorted and only include the necessary public parts + switch k := privkey.(type) { + case *rsa.PrivateKey: + return x509.MarshalPKCS1PrivateKey(k), nil + case *ecdsa.PrivateKey: + return x509.MarshalECPrivateKey(k) + default: + // this is unreachable because we know the types that we pass in + log.Printf("keytype: %t, %+v\n", privkey, privkey) + panic(ErrInvalidPublicKey) + //return nil, nil + } +} + +func marshalDERPrivateKey(privkey PrivateKey) (*pem.Block, error) { + var typ string + var bytes []byte + var err error + + switch k := privkey.(type) { + case *rsa.PrivateKey: + if 0 == mathrand.Intn(2) { + typ = "PRIVATE KEY" + bytes, err = x509.MarshalPKCS8PrivateKey(k) + if nil != err { + return nil, err + } + } else { + typ = "RSA PRIVATE KEY" + bytes = x509.MarshalPKCS1PrivateKey(k) + } + return &pem.Block{ + Type: typ, + Bytes: bytes, + }, nil + case *ecdsa.PrivateKey: + if 0 == mathrand.Intn(2) { + typ = "PRIVATE KEY" + bytes, err = x509.MarshalPKCS8PrivateKey(k) + } else { + typ = "EC PRIVATE KEY" + bytes, err = x509.MarshalECPrivateKey(k) + } + if nil != err { + return nil, err + } + return &pem.Block{ + Type: typ, + Bytes: bytes, + }, nil + default: + // this is unreachable because we know the types that we pass in + log.Printf("keytype: %t, %+v\n", privkey, privkey) + panic(ErrInvalidPublicKey) + //return nil, nil + } +} + +// MarshalPEMPrivateKey outputs the given private key as ASN.1 PEM +func MarshalPEMPrivateKey(privkey PrivateKey) ([]byte, error) { + block, err := marshalDERPrivateKey(privkey) + if nil != err { + return nil, err + } + return pem.EncodeToMemory(block), nil +} + +// MarshalECPrivateKey will output the given private key as JWK +func MarshalECPrivateKey(k *ecdsa.PrivateKey) []byte { + crv := k.Curve.Params().Name + d := base64.RawURLEncoding.EncodeToString(k.D.Bytes()) + x := base64.RawURLEncoding.EncodeToString(k.X.Bytes()) + y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes()) + return []byte(fmt.Sprintf( + `{"crv":%q,"d":%q,"kty":"EC","x":%q,"y":%q}`, + crv, d, x, y, + )) +} + +// MarshalRSAPrivateKey will output the given private key as JWK +func MarshalRSAPrivateKey(pk *rsa.PrivateKey) []byte { + e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pk.E)).Bytes()) + n := base64.RawURLEncoding.EncodeToString(pk.N.Bytes()) + d := base64.RawURLEncoding.EncodeToString(pk.D.Bytes()) + p := base64.RawURLEncoding.EncodeToString(pk.Primes[0].Bytes()) + q := base64.RawURLEncoding.EncodeToString(pk.Primes[1].Bytes()) + dp := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dp.Bytes()) + dq := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dq.Bytes()) + qi := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Qinv.Bytes()) + return []byte(fmt.Sprintf( + `{"d":%q,"dp":%q,"dq":%q,"e":%q,"kty":"RSA","n":%q,"p":%q,"q":%q,"qi":%q}`, + d, dp, dq, e, n, p, q, qi, + )) +}