diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.ignore @@ -0,0 +1 @@ +vendor diff --git a/go-test.sh b/go-test.sh new file mode 100644 index 0000000..2884b66 --- /dev/null +++ b/go-test.sh @@ -0,0 +1 @@ +go test -mod=vendor -v ./... diff --git a/mockid.go b/mockid.go index 366c107..3bd8488 100644 --- a/mockid.go +++ b/mockid.go @@ -5,10 +5,12 @@ import ( "fmt" "io/ioutil" "log" + "math/rand" "net/http" "net/url" "os" "strconv" + "time" "git.coolaj86.com/coolaj86/go-mockid/mockid" "git.rootprojects.org/root/keypairs" @@ -21,6 +23,8 @@ func main() { var port int var host string + rand.Seed(time.Now().UnixNano()) + portFlag := flag.Int("port", 0, "Port on which the HTTP server should run") urlFlag := flag.String("url", "", "Outward-facing address, such as https://example.com") prefixFlag := flag.String("jwkspath", "", "The path to the JWKs storage directory") diff --git a/mockid/marshal.go b/mockid/marshal.go index d937b3e..7b7dbdf 100644 --- a/mockid/marshal.go +++ b/mockid/marshal.go @@ -3,10 +3,13 @@ package mockid import ( "crypto/ecdsa" "crypto/rsa" + "crypto/x509" "encoding/base64" + "encoding/pem" "fmt" "log" "math/big" + mathrand "math/rand" "git.rootprojects.org/root/keypairs" ) @@ -27,6 +30,75 @@ func MarshalJWKPrivateKey(privkey keypairs.PrivateKey) []byte { } } +// MarshalDERPrivateKey outputs the given private key as ASN.1 DER +func MarshalDERPrivateKey(privkey keypairs.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(keypairs.ErrInvalidPublicKey) + return nil, nil + } +} + +func marshalDERPrivateKey(privkey keypairs.PrivateKey) (*pem.Block, error) { + var typ string + var bytes []byte + var err error + + switch k := privkey.(type) { + case *rsa.PrivateKey: + if 0 == mathrand.Intn(1) { + 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(1) { + 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(keypairs.ErrInvalidPublicKey) + return nil, nil + } +} + +// MarshalPEMPrivateKey outputs the given private key as ASN.1 PEM +func MarshalPEMPrivateKey(privkey keypairs.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 diff --git a/mockid/mockid.go b/mockid/mockid.go index c8ca896..cf1c7ea 100644 --- a/mockid/mockid.go +++ b/mockid/mockid.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io" "math/big" "net/http" "net/url" @@ -21,6 +22,9 @@ import ( //jwt "github.com/dgrijalva/jwt-go" ) +// TestMain will overwrite this +var rndsrc io.Reader = rand.Reader + type PublicJWK struct { Crv string `json:"crv"` KeyID string `json:"kid,omitempty"` @@ -145,7 +149,7 @@ func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte { case *rsa.PrivateKey: panic("TODO: implement rsa sign") case *ecdsa.PrivateKey: - r, s, _ := ecdsa.Sign(rand.Reader, k, hash[:]) + r, s, _ := ecdsa.Sign(rndsrc, k, hash[:]) rb := r.Bytes() fmt.Println("debug:") fmt.Println(r, s) diff --git a/mockid/mockid_test.go b/mockid/mockid_test.go index e2b672e..865244d 100644 --- a/mockid/mockid_test.go +++ b/mockid/mockid_test.go @@ -3,22 +3,41 @@ package mockid import ( "crypto/ecdsa" "crypto/elliptic" - "crypto/rand" + "crypto/rsa" "encoding/json" + "errors" "fmt" "io/ioutil" + "log" + mathrand "math/rand" "net/http" "net/http/httptest" "net/url" "os" "testing" + + "git.rootprojects.org/root/keypairs" //keypairs "github.com/big-squid/go-keypairs" //"github.com/big-squid/go-keypairs/keyfetch/uncached" ) var srv *httptest.Server +type TestReader struct{} + +func (TestReader) Read(p []byte) (n int, err error) { + return mathrand.Read(p) +} + +var testrnd = TestReader{} + +func init() { + rndsrc = testrnd +} + func TestMain(m *testing.M) { + mathrand.Seed(0) // Predictable results + os.Setenv("SALT", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") jwksPrefix := "public-jwks" err := os.MkdirAll(jwksPrefix, 0755) @@ -27,7 +46,7 @@ func TestMain(m *testing.M) { os.Exit(1) } - privkey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + privkey, _ := ecdsa.GenerateKey(elliptic.P256(), rndsrc) mux := Route(jwksPrefix, privkey) srv = httptest.NewServer(mux) @@ -38,7 +57,7 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func TestTest(t *testing.T) { +func TestGenerateJWK(t *testing.T) { client := srv.Client() urlstr, _ := url.Parse(srv.URL + "/private.jwk.json") //fmt.Println("URL:", srv.URL, urlstr) @@ -49,12 +68,14 @@ func TestTest(t *testing.T) { if nil != err { //t.Fatal(err) t.Error(err) + return } data, err := ioutil.ReadAll(res.Body) if nil != err { //t.Fatal(err) t.Error(err) + return } jwk := map[string]string{} @@ -62,10 +83,70 @@ func TestTest(t *testing.T) { if nil != err { //t.Fatal(err) t.Error(err) + return } if "" == jwk["d"] { t.Fatal("Missing key 'd' from supposed private key") } + + key, err := keypairs.ParsePrivateKey(data) + if nil != err { + t.Error(err) + return + } + + switch key.(type) { + case *rsa.PrivateKey: + // no-op + log.Println("is RSA") + case *ecdsa.PrivateKey: + // no-op + log.Println("is EC") + default: + t.Fatal(errors.New("impossible key type")) + } + //fmt.Printf("%#v\n", jwk) } + +func TestGeneratePEM(t *testing.T) { + client := srv.Client() + urlstr, _ := url.Parse(srv.URL + "/priv.pem") + //fmt.Println("URL:", srv.URL, urlstr) + res, err := client.Do(&http.Request{ + Method: "POST", + URL: urlstr, + }) + if nil != err { + //t.Fatal(err) + t.Error(err) + return + } + + data, err := ioutil.ReadAll(res.Body) + if nil != err { + //t.Fatal(err) + t.Error(err) + return + } + + key, err := ParsePEMPrivateKey(data) + if nil != err { + t.Error(err) + return + } + + switch key.(type) { + case *rsa.PrivateKey: + // no-op + log.Println("is RSA") + case *ecdsa.PrivateKey: + // no-op + log.Println("is EC") + default: + t.Fatal(errors.New("impossible key type")) + } + + //fmt.Printf("%#v\n", key) +} diff --git a/mockid/parse.go b/mockid/parse.go index 35c657b..ad565d0 100644 --- a/mockid/parse.go +++ b/mockid/parse.go @@ -1,6 +1,15 @@ package mockid -import "strconv" +import ( + "strconv" + + "git.rootprojects.org/root/keypairs" +) + +func ParsePEMPrivateKey(block []byte) (keypairs.PrivateKey, error) { + // TODO do not parse DER or JWK + return keypairs.ParsePrivateKey(block) +} func parseExp(exp string) (int, error) { if "" == exp { diff --git a/mockid/route.go b/mockid/route.go index b8c4d54..00a73c3 100644 --- a/mockid/route.go +++ b/mockid/route.go @@ -1,16 +1,20 @@ package mockid import ( - "crypto/rand" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rsa" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/base64" "encoding/json" + "errors" "fmt" + "io" "io/ioutil" "log" + mathrand "math/rand" "net/http" "os" "path/filepath" @@ -180,17 +184,100 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { fmt.Fprintf(w, token) }) + getKty := func(r *http.Request) (string, error) { + tok := make(map[string]interface{}) + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&tok) + if nil != err && io.EOF != err { + log.Printf("json decode error: %s", err) + return "", errors.New("Bad Request: invalid json body") + } + defer r.Body.Close() + + kty, _ := tok["kty"].(string) + if "" == kty { + if 0 == mathrand.Intn(2) { + kty = "RSA" + } else { + kty = "EC" + } + } + return kty, nil + } + http.HandleFunc("/private.jwk.json", func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s", r.Method, r.URL.Path) + if "POST" != r.Method { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + kty, err := getKty(r) + if nil != err { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var privkey keypairs.PrivateKey + if "RSA" == kty { + keylen := 2048 + privkey, _ = rsa.GenerateKey(rndsrc, keylen) + } else { + privkey, _ = ecdsa.GenerateKey(elliptic.P256(), rndsrc) + } + + jwk := MarshalJWKPrivateKey(privkey) + w.Write(jwk) + }) + + http.HandleFunc("/priv.der", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s\n", r.Method, r.URL.Path) if "POST" != r.Method { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } - keylen := 2048 - privkey, _ := rsa.GenerateKey(rand.Reader, keylen) - jwk := string(MarshalJWKPrivateKey(privkey)) - fmt.Fprintf(w, jwk) + kty, err := getKty(r) + if nil != err { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var privkey keypairs.PrivateKey + if "RSA" == kty { + keylen := 2048 + privkey, _ = rsa.GenerateKey(rndsrc, keylen) + } else { + privkey, _ = ecdsa.GenerateKey(elliptic.P256(), rndsrc) + } + + der, _ := MarshalDERPrivateKey(privkey) + w.Write(der) + }) + + http.HandleFunc("/priv.pem", func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s\n", r.Method, r.URL.Path) + if "POST" != r.Method { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + kty, err := getKty(r) + if nil != err { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var privkey keypairs.PrivateKey + if "RSA" == kty { + keylen := 2048 + privkey, _ = rsa.GenerateKey(rndsrc, keylen) + } else { + privkey, _ = ecdsa.GenerateKey(elliptic.P256(), rndsrc) + } + + privpem, _ := MarshalPEMPrivateKey(privkey) + w.Write(privpem) }) http.HandleFunc("/inspect_token", func(w http.ResponseWriter, r *http.Request) {