Skip to content

Commit

Permalink
Add support for X.509 and a website (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
MicahParks committed Dec 6, 2023
1 parent 405fc35 commit 30b57c3
Show file tree
Hide file tree
Showing 66 changed files with 6,171 additions and 789 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
config.*json
node_modules
237 changes: 86 additions & 151 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,162 +1,97 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/MicahParks/jwkset)](https://goreportcard.com/report/github.com/MicahParks/jwkset) [![Go Reference](https://pkg.go.dev/badge/github.com/MicahParks/jwkset.svg)](https://pkg.go.dev/github.com/MicahParks/jwkset)
# JWK Set
This is a JWK Set (JWKS or jwks) implementation. For a JWK Set client,
see [`github.com/MicahParks/keyfunc`](https://github.com/MicahParks/keyfunc). Cryptographic keys can be added, deleted,
and read from the JWK Set. A JSON representation of the JWK Set can be created for hosting via HTTPS. This project
includes an in-memory storage implementation, but an interface is provided for more advanced use cases. For this
implementation, a key ID (`kid`) is required.

This project only depends on packages from the standard Go library. It has no external dependencies.

The following key types have a JSON representation:

| Key type | Go private key type | Go public key type | External link |
|----------|----------------------------------------------------------------------|--------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| `EC` | [`*ecdsa.PrivateKey`](https://pkg.go.dev/crypto/ecdsa#PrivateKey) | [`*ecdsa.PublicKey`](https://pkg.go.dev/crypto/ecdsa#PublicKey) | [Elliptic Curve Digital Signature Algorithm (ECDSA)](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) |
| `OKP` | [`ed25519.PrivateKey`](https://pkg.go.dev/crypto/ed25519#PrivateKey) | [`ed25519.PublicKey`](https://pkg.go.dev/crypto/ed25519#PublicKey) | [Edwards-curve Digital Signature Algorithm (EdDSA)](https://en.wikipedia.org/wiki/EdDSA) |
| `RSA` | [`*rsa.PrivateKey`](https://pkg.go.dev/crypto/rsa#PrivateKey) | [`*rsa.PublicKey`](https://pkg.go.dev/crypto/rsa#PublicKey) | [Rivest–Shamir–Adleman (RSA)](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) |
| `oct` | `[]byte` | none | |

Only the Go types listed in this table have a JSON representation. If you would like support for another key type,
please open an issue on GitHub.

# Example HTTP server
```go
package main

import (
"context"
"crypto/rand"
"crypto/rsa"
"log"
"net/http"
"os"

"github.com/MicahParks/jwkset"
)

const (
logFmt = "%s\nError: %s"
)

func main() {
ctx := context.Background()
logger := log.New(os.Stdout, "", 0)

jwkSet := jwkset.NewMemory[any]()

key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
logger.Fatalf(logFmt, "Failed to generate RSA key.", err)
}

err = jwkSet.Store.WriteKey(ctx, jwkset.NewKey[any](key, "my-key-id"))
if err != nil {
logger.Fatalf(logFmt, "Failed to store RSA key.", err)
}

http.HandleFunc("/jwks.json", func(writer http.ResponseWriter, request *http.Request) {
// TODO Cache the JWK Set so storage isn't called for every request.
response, err := jwkSet.JSONPublic(request.Context())
if err != nil {
logger.Printf(logFmt, "Failed to get JWK Set JSON.", err)
writer.WriteHeader(http.StatusInternalServerError)
return
}

writer.Header().Set("Content-Type", "application/json")
_, _ = writer.Write(response)
})

logger.Print("Visit: http://localhost:8080/jwks.json")
logger.Fatalf("Failed to listen and serve: %s", http.ListenAndServe(":8080", nil))
}
[![Go Reference](https://pkg.go.dev/badge/github.com/MicahParks/jwkset.svg)](https://pkg.go.dev/github.com/MicahParks/jwkset)

# JWK Set (JSON Web Key Set)

This is a JWK Set (JSON Web Key Set) implementation written in Golang.

If you would like to generate or validate a JWK without writing any Golang code, please visit
the [Generate a JWK Set](#generate-a-jwk-set) section.

If you would like to have a JWK Set client to help verify JWTs without writing any Golang code, you can use the
[JWK Set Client Proxy (JCP) project](https://github.com/MicahParks/jcp) perform JWK Set client operations in the
language of your choice using an OpenAPI interface.

# Generate a JWK Set

If you would like to generate a JWK Set without writing Golang code, this project publishes utilities to generate a JWK
Set from:

* PEM encoded X.509 Certificates
* PEM encoded public keys
* PEM encoded private keys

The PEM block type is used to infer which key type to decode. Reference the [Supported keys](#supported-keys) section
for a list of supported cryptographic key types.

## Website

Please visit [https://jwkset.com](https://jwkset.com) to use the web interface for this project. You can self-host this
website by following the instructions in the `README.md` in the [website](https://github.com/MicahParks/jwkset/website)
directory.

## Command line

Gather your PEM encoded keys or certificates and use the `cmd/jwksetinfer` command line tool to generate a JWK Set.

**Install**

```
go install github.com/MicahParks/jwkset/cmd/jwksetinfer@latest
```

**Usage**

# Example for marshalling a single key to a JSON Web Key
```go
package main

import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"log"
"os"

"github.com/MicahParks/jwkset"
)

const logFmt = "%s\nError: %s"

func main() {
logger := log.New(os.Stdout, "", 0)

// Create an EdDSA key.
_, private, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
logger.Fatalf(logFmt, "Failed to generate EdDSA key.", err)
}

// Wrap the key in the appropriate Go type.
meta := jwkset.NewKey(private, "my-key-id")

// Create the approrpiate options to include the private key material in the JSON representation.
options := jwkset.KeyMarshalOptions{
AsymmetricPrivate: true,
}

// Marshal the key to a different Go type that can be serialized to JSON.
marshal, err := jwkset.KeyMarshal(meta, options)
if err != nil {
logger.Fatalf(logFmt, "Failed to marshal key.", err)
}

// Marshal the new type to JSON.
j, err := json.MarshalIndent(marshal, "", " ")
if err != nil {
logger.Fatalf(logFmt, "Failed to marshal JSON.", err)
}
println(string(j))

// Unmarshal the raw JSON into a Go type that can be deserialized into a key.
err = json.Unmarshal(j, &marshal)
if err != nil {
logger.Fatalf(logFmt, "Failed to unmarshal JSON.", err)
}

// Create the appropriate options to include the private key material in the deserialization.
//
// If this option is not provided, the resulting key will be of the type ed25519.PublicKey.
unmarshalOptions := jwkset.KeyUnmarshalOptions{
AsymmetricPrivate: true,
}

// Convert the Go type back into a key with metadata.
meta, err = jwkset.KeyUnmarshal(marshal, unmarshalOptions)
if err != nil {
logger.Fatalf(logFmt, "Failed to unmarshal key.", err)
}

// Print the key ID.
println(meta.KeyID)
}
```
jwksetinfer mykey.pem mycert.crt
```

# Supported keys

This project supports the following key types:

* [Edwards-curve Digital Signature Algorithm (EdDSA)](https://en.wikipedia.org/wiki/EdDSA) (Ed25519 only)
* Go Types: `ed25519.PrivateKey` and `ed25519.PublicKey`
* [Elliptic-curve Diffie–Hellman (ECDH)](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman) (X25519
only)
* Go Types: `*ecdh.PrivateKey` and `*ecdh.PublicKey`
* [Elliptic Curve Digital Signature Algorithm (ECDSA)](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm)
* Go Types: `*ecdsa.PrivateKey` and `*ecdsa.PublicKey`
* [Rivest–Shamir–Adleman (RSA)](https://en.wikipedia.org/wiki/RSA_(cryptosystem))
* Go Types: `*rsa.PrivateKey` and `*rsa.PublicKey`
* [HMAC](https://en.wikipedia.org/wiki/HMAC), [AES Key Wrap](https://en.wikipedia.org/wiki/Key_Wrap), and other
symmetric keys
* Go Type: `[]byte`

Cryptographic keys can be added, deleted, and read from the JWK Set. A JSON representation of the JWK Set can be created
for hosting via HTTPS. This project includes an in-memory storage implementation, but an interface is provided for more
advanced use cases.

# Notes

This project aims to implement the relevant RFCs to the fullest extent possible using the Go standard library, but does
not implement any cryptographic algorithms itself.

* RFC 8037 adds support for `Ed448`, `X448`, and `secp256k1`, but there is no Golang standard library support for these
key types.
* RFC 7518 specifies that `Base64urlUInt` must use the "minimum number of octets" to represent the number. This can lead
to a problem with parsing JWK made by other projects that may contain leading zeros in the
non-compliant `Base64urlUInt` encoding. This error happens during JWK validation and will look
like: `failed to validate JWK: marshaled JWK does not match original JWK`. To work around this, please modify the
JWK's JSON to remove the leading zeros for a proper `Base64urlUInt` encoding. If you need help doing this, please open
a GitHub issue.
* This project does not currently support JWK Set encryption using JWE. This would involve implementing the relevant JWE
specifications. It may be implemented in the future if there is interest. Open a GitHub issue to express interest.

# Test coverage
Test coverage is currently `99%`.

```
$ go test -cover -race
$ go test -cover
PASS
coverage: 98.5% of statements
ok github.com/MicahParks/jwkset 0.031s
coverage: 85.5% of statements
ok github.com/MicahParks/jwkset 0.013s
```

# References
This project was built and tested using various RFCs and services. The services are listed below:
* [mkjwk.org](https://github.com/mitreid-connect/mkjwk.org)
# See also

See also:
* [`github.com/MicahParks/keyfunc`](https://github.com/MicahParks/keyfunc)
* [`github.com/golang-jwt/jwt/v4`](https://github.com/golang-jwt/jwt)
* [`github.com/MicahParks/jcp`](https://github.com/MicahParks/jcp)
* [`github.com/MicahParks/keyfunc`](https://github.com/MicahParks/keyfunc)
38 changes: 38 additions & 0 deletions cmd/gen_ec/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"log"
"os"
)

const (
logFmt = "%s\nError: %s"
privFile = "ec256SEC1Priv.pem"
)

func main() {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatalf(logFmt, "Failed to generate EC key.", err)
}

pemBytes, err := x509.MarshalECPrivateKey(priv)
if err != nil {
log.Fatalf(logFmt, "Failed to marshal EC private key.", err)
}
block := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: pemBytes,
}
out := pem.EncodeToMemory(block)

err = os.WriteFile(privFile, out, 0644)
if err != nil {
log.Fatalf(logFmt, "Failed to write EC private key.", err)
}
}
46 changes: 46 additions & 0 deletions cmd/gen_pkcs1/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"log"
"os"
)

const (
logFmt = "%s\nError: %s"
privFile = "rsa2048PKCS1Priv.pem"
pubFile = "rsa2048PKCS1Pub.pem"
)

func main() {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatalf(logFmt, "Failed to generate RSA key.", err)
}

block := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(priv),
}
out := pem.EncodeToMemory(block)

err = os.WriteFile(privFile, out, 0644)
if err != nil {
log.Fatalf(logFmt, "Failed to write RSA private key.", err)
}

pub := &priv.PublicKey
block = &pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: x509.MarshalPKCS1PublicKey(pub),
}
out = pem.EncodeToMemory(block)

err = os.WriteFile(pubFile, out, 0644)
if err != nil {
log.Fatalf(logFmt, "Failed to write RSA public key.", err)
}
}
5 changes: 5 additions & 0 deletions cmd/jwksetinfer/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/MicahParks/jwkset/cmd/jwksetinfer

go 1.21.4

require github.com/MicahParks/jwkset v0.3.1
2 changes: 2 additions & 0 deletions cmd/jwksetinfer/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/MicahParks/jwkset v0.3.1 h1:DIVazR/elD8CLWPblrVo610TzovIDYMcvlM4X0UT0vQ=
github.com/MicahParks/jwkset v0.3.1/go.mod h1:Ob0sxSgMmQZFg4GO59PVBnfm+jtdQ1MJbfZDU90tEwM=
6 changes: 6 additions & 0 deletions cmd/jwksetinfer/go.work
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
go 1.21.4

use (
../..
.
)

0 comments on commit 30b57c3

Please sign in to comment.