package mockid import ( "crypto/sha1" "crypto/sha256" "crypto/sha512" "crypto/subtle" "encoding/base64" "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" "os" "path/filepath" "strconv" "strings" "time" "git.coolaj86.com/coolaj86/go-mockid/kvdb" "git.coolaj86.com/coolaj86/go-mockid/mockid/api" "git.coolaj86.com/coolaj86/go-mockid/xkeypairs" "git.rootprojects.org/root/keypairs" "git.rootprojects.org/root/keypairs/keyfetch" "github.com/google/uuid" ua "github.com/mileusna/useragent" ) type HTTPResponse struct { Error string `json:"error"` Success bool `json:"success"` } type TokenResponse struct { Receipt string `json:"receipt"` HTTPResponse } type OTPResponse struct { OTP HTTPResponse } var tokenPrefix string var contactPrefix string // Route returns an HTTP Mux containing the full API func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { Init() contactKV := kvdb.KVDB{ Prefix: jwksPrefix + "/contacts", Ext: "eml.json", } // TODO get from main() tokenPrefix = jwksPrefix + "/tokens" contactPrefix = jwksPrefix + "/contacts" for _, pre := range []string{tokenPrefix, contactPrefix} { if err := os.MkdirAll(pre, 0750); nil != err { panic(err) } } pubkey := keypairs.NewPublicKey(privkey.Public()) http.HandleFunc("/api/new-hashcash", func(w http.ResponseWriter, r *http.Request) { if "POST" != r.Method { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } indexURL := getBaseURL(r) + "/api/directory" w.Header().Set("Link", "<"+indexURL+">;rel=\"index\"") w.Header().Set("Date", time.Now().Format(http.TimeFormat)) // disable caching in every possible way w.Header().Set("Expires", time.Now().Format(http.TimeFormat)) w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store") w.Header().Set("Pragma", "no-cache") // add reasonable security options w.Header().Set("Strict-Transport-Security", "max-age=604800") w.Header().Set("X-Frame-Options", "DENY") h := issueHashcash(w, r) b, _ := json.Marshal(h) w.Write(b) }) http.HandleFunc("/api/authn/meta", func(w http.ResponseWriter, r *http.Request) { if "GET" != r.Method { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } query := r.URL.Query() contact := strings.Replace(strings.TrimPrefix(query.Get("contact"), "mailto:"), " ", "+", -1) if "" == contact { fmt.Println("got here 3a") http.Error(w, "Bad Request", http.StatusBadRequest) b, _ := json.Marshal(&HTTPResponse{ Error: "missing require query parameter 'contact'", }) w.Write(b) return } _, ok, err := contactKV.Load(contact) if nil != err { fmt.Println("got here 3b") fmt.Fprintf(os.Stderr, "bad things:", err.Error()) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } if ok { b, _ := json.Marshal(&HTTPResponse{ Success: true, }) w.Write(b) return } b, _ := json.Marshal(&HTTPResponse{ Error: "not found", }) w.Write(b) }) http.HandleFunc("/api/authn/verify", func(w http.ResponseWriter, r *http.Request) { baseURL := getBaseURL(r) query := r.URL.Query() contact := strings.Replace(strings.TrimPrefix(query.Get("contact"), "mailto:"), " ", "+", -1) fmt.Println("contact:", contact) addr := strings.Split(r.RemoteAddr, ":")[0] receipt, err := startVerification( baseURL, contact, r.Header.Get("User-Agent"), addr, ) if nil != err { http.Error(w, "Bad Request", http.StatusBadRequest) msg, _ := json.Marshal(err.Error()) fmt.Fprintf(w, `{"error":%s}`+"\n", msg) return } b, _ := json.Marshal(&TokenResponse{ HTTPResponse: HTTPResponse{Success: true}, Receipt: receipt, }) w.Write(b) }) http.HandleFunc("/api/authn/consume", func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() otpSecret := query.Get("secret") secret, b64err := base64.RawURLEncoding.DecodeString(otpSecret) receipt := query.Get("receipt") fmt.Println("secret:", otpSecret, secret, b64err) if (0 == len(secret) || nil != b64err) && "" == receipt { http.Error(w, "Bad Request", http.StatusBadRequest) msg, _ := json.Marshal("missing token secret and/or token receipt") fmt.Fprintf(w, `{"error":%s}`+"\n", msg) return } addr := strings.Split(r.RemoteAddr, ":")[0] agent := r.Header.Get("User-Agent") var otp *OTP var err error if 0 != len(secret) && nil == b64err { if "" != receipt { _, rcpt := hashOTPSecret(secret) if rcpt != receipt { http.Error(w, "Bad Request", http.StatusBadRequest) fmt.Fprintf(w, "%s", "otp secret and receipt do not match") return } } otp, err = consumeOTPSecret("TODO", secret, agent, addr) } else if 0 != len(receipt) { otp, err = consumeOTPReceipt("TODO", receipt, agent, addr) } if nil != err { // TODO propagate error types http.Error(w, "Bad Request", http.StatusBadRequest) fmt.Fprintf(w, "%s", err) return } b, _ := json.Marshal(&OTPResponse{ HTTPResponse: HTTPResponse{Success: true}, OTP: *otp, }) w.Write(b) }) http.HandleFunc("/api/new-nonce", requireHashcash(func(w http.ResponseWriter, r *http.Request) { indexURL := getBaseURL(r) + "/api/directory" w.Header().Set("Link", "<"+indexURL+">;rel=\"index\"") w.Header().Set("Date", time.Now().Format(http.TimeFormat)) // disable caching in every possible way w.Header().Set("Expires", time.Now().Format(http.TimeFormat)) w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store") w.Header().Set("Pragma", "no-cache") w.Header().Set("Strict-Transport-Security", "max-age=604800") w.Header().Set("X-Frame-Options", "DENY") issueNonce(w, r) })) http.HandleFunc("/api/test-hashcash", func(w http.ResponseWriter, r *http.Request) { if err := UseHashcash(r.Header.Get("Hashcash"), r.Host); nil != err { b, _ := json.Marshal(&HTTPResponse{ Error: err.Error(), }) w.Write(b) return } b, _ := json.Marshal(&HTTPResponse{ Success: true, }) w.Write(b) }) type NewAccount struct { Contact []string `json:"contact"` TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` } http.HandleFunc("/api/new-account", func(w http.ResponseWriter, r *http.Request) { myURL := getBaseURL(r) + r.URL.Path jws := &xkeypairs.JWS{} decoder := json.NewDecoder(r.Body) if err := decoder.Decode(jws); nil != err { http.Error(w, "Bad Request", http.StatusBadRequest) return } defer r.Body.Close() if err := jws.DecodeComponents(); nil != err { http.Error(w, "Bad Request", http.StatusBadRequest) fmt.Fprintf(w, `{"error":%q}`+"\n", err) return } kid, _ := jws.Header["kid"].(string) if "" != kid { http.Error(w, "Bad Request", http.StatusBadRequest) fmt.Fprintf(w, `{"error":"jws must include protected jwk, which is mutually exclusive from kid"}`+"\n") return } alg, _ := jws.Header["alg"].(string) if !strings.HasSuffix(alg, "256") { http.Error(w, "Bad Request", http.StatusBadRequest) fmt.Fprintf(w, `{"error":"invalid jws protected algorithm"}`+"\n") return } nonce, _ := jws.Header["nonce"].(string) if !useNonce(nonce) { http.Error(w, "Bad Request", http.StatusBadRequest) fmt.Fprintf(w, `{"error":"invalid jws protected nonce"}`+"\n") return } jwsURL, _ := jws.Header["url"].(string) if myURL != jwsURL { http.Error(w, "Bad Request", http.StatusBadRequest) fmt.Fprintf(w, `{"error":"invalid jws protected target URL"}`+"\n") return } ok, err := xkeypairs.VerifyClaims(nil, jws) if nil != err || !ok { if nil != err { log.Printf("jws verify error: %s", err) } http.Error(w, "Bad Request", http.StatusBadRequest) fmt.Fprintf(w, `{"error":"could not verify JWS claims"}`+"\n") return } contacts, _ := jws.Claims["contact"].([]string) contact := "" if 1 == len(contacts) { contact = contacts[0] } baseURL := getBaseURL(r) addr := strings.Split(r.RemoteAddr, ":")[0] receipt, err := startVerification( baseURL, contact, r.Header.Get("User-Agent"), addr, ) if nil != err { http.Error(w, "Bad Request", http.StatusBadRequest) msg, _ := json.Marshal(err.Error()) fmt.Fprintf(w, `{"error":%s}`+"\n", msg) return } fmt.Fprintf(w, `{ "success": true, "error": "", "receipt":, "%s" }%s`, receipt, "\n") }) // TODO use chi http.HandleFunc("/verify/", requireNonce(func(w http.ResponseWriter, r *http.Request) { parts := strings.Split(r.URL.Path, "/") if 3 != len(parts) { http.Error(w, "invalid url path", http.StatusBadRequest) return } secret, err := base64.RawURLEncoding.DecodeString(parts[2]) if nil != err { http.Error(w, "Bad Request", http.StatusBadRequest) fmt.Fprintf(w, "%s", err) return } addr := strings.Split(r.RemoteAddr, ":")[0] if _, err := consumeOTPSecret("TODO", secret, r.Header.Get("User-Agent"), addr); nil != err { http.Error(w, "Internal Server Error", http.StatusInternalServerError) fmt.Fprintf(w, "%s", err) return } fmt.Fprintf(w, `{ "success": true, "error": "" }%s`, "\n") })) http.HandleFunc("/api/jwks", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s %s", r.Method, r.Host, r.URL.Path) if "POST" != r.Method { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } tok := make(map[string]interface{}) decoder := json.NewDecoder(r.Body) err := decoder.Decode(&tok) if nil != err { http.Error(w, "Bad Request: invalid json", http.StatusBadRequest) return } defer r.Body.Close() // TODO better, JSON error messages if _, ok := tok["d"]; ok { http.Error(w, "Bad Request: private key", http.StatusBadRequest) return } kty, _ := tok["kty"].(string) switch kty { case "EC": postEC(jwksPrefix, tok, w, r) case "RSA": postRSA(jwksPrefix, tok, w, r) default: http.Error(w, "Bad Request: only EC and RSA keys are supported", http.StatusBadRequest) return } }) http.HandleFunc("/access_token", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s\n", r.Method, r.URL.Path) _, _, token := GenToken(getBaseURL(r), privkey, r.URL.Query()) fmt.Fprintf(w, token) }) // TODO add /debug prefix http.HandleFunc("/debug/private.jwk.json", api.GeneratePrivateJWK) http.HandleFunc("/debug/priv.der", api.GeneratePrivateDER) http.HandleFunc("/debug/priv.pem", api.GeneratePrivatePEM) http.HandleFunc("/debug/public.jwk.json", api.GeneratePublicJWK) http.HandleFunc("/debug/pub.der", api.GeneratePublicDER) http.HandleFunc("/debug/pub.pem", api.GeneratePublicPEM) http.HandleFunc("/debug/jose.jws.json", api.SignJWS) http.HandleFunc("/debug/jose.jws.jwt", api.SignJWT) http.HandleFunc("/debug/verify", api.Verify) http.HandleFunc("/inspect_token", func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") log.Printf("%s %s %s\n", r.Method, r.URL.Path, token) if "" == token { token = r.URL.Query().Get("access_token") if "" == token { http.Error(w, "Bad Format: missing Authorization header and 'access_token' query", http.StatusBadRequest) return } } else { parts := strings.Split(token, " ") if 2 != len(parts) { http.Error(w, "Bad Format: expected Authorization header to be in the format of 'Bearer '", http.StatusBadRequest) return } token = parts[1] } parts := strings.Split(token, ".") if 3 != len(parts) { http.Error(w, "Bad Format: token should be in the format of ..", http.StatusBadRequest) return } protected64 := parts[0] payload64 := parts[1] signature64 := parts[2] protectedB, err := base64.RawURLEncoding.DecodeString(protected64) if nil != err { http.Error(w, "Bad Format: token's header should be URL-safe base64 encoded", http.StatusBadRequest) return } payloadB, err := base64.RawURLEncoding.DecodeString(payload64) if nil != err { http.Error(w, "Bad Format: token's payload should be URL-safe base64 encoded", http.StatusBadRequest) return } // TODO verify signature sig, err := base64.RawURLEncoding.DecodeString(signature64) if nil != err { http.Error(w, "Bad Format: token's signature should be URL-safe base64 encoded", http.StatusBadRequest) return } errors := []string{} protected := map[string]interface{}{} err = json.Unmarshal(protectedB, &protected) if nil != err { http.Error(w, "Bad Format: token's header should be URL-safe base64-encoded JSON", http.StatusBadRequest) return } kid, kidOK := protected["kid"].(string) // TODO parse jwkM _, jwkOK := protected["jwk"] if !kidOK && !jwkOK { errors = append(errors, "must have either header.kid or header.jwk") } data := map[string]interface{}{} err = json.Unmarshal(payloadB, &data) if nil != err { http.Error(w, "Bad Format: token's payload should be URL-safe base64-encoded JSON", http.StatusBadRequest) return } iss, issOK := data["iss"].(string) if !jwkOK && !issOK { errors = append(errors, "payload.iss must exist to complement header.kid") } pub, err := keyfetch.OIDCJWK(kid, iss) if nil != err { fmt.Println("couldn't fetch pub key:") fmt.Println(err) } fmt.Println("fetched pub key:") fmt.Println(pub) hash := sha256.Sum256([]byte(fmt.Sprintf("%s.%s", protected64, payload64))) verified := JOSEVerify(pub, hash[:], sig) inspected := &InspectableToken{ Public: pub, Protected: protected, Payload: data, Signature: signature64, Verified: verified, Errors: errors, } tokenB, _ := json.MarshalIndent(inspected, "", " ") if nil != err { http.Error(w, "Bad Format: malformed token, or malformed jwk at issuer url", http.StatusInternalServerError) return } fmt.Fprintf(w, string(tokenB)) }) http.HandleFunc("/authorization_header", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s\n", r.Method, r.URL.Path) var header string headers, _ := r.URL.Query()["header"] if 0 == len(headers) { header = "Authorization" } else { header = headers[0] } var prefix string prefixes, _ := r.URL.Query()["prefix"] if 0 == len(prefixes) { prefix = "Bearer " } else { prefix = prefixes[0] } _, _, token := GenToken(getBaseURL(r), privkey, r.URL.Query()) fmt.Fprintf(w, "%s: %s%s", header, prefix, token) }) http.HandleFunc("/key.jwk.json", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL.Path) jwk := string(xkeypairs.MarshalJWKPrivateKey(privkey)) jwk = strings.Replace(jwk, `{"`, `{ "`, 1) jwk = strings.Replace(jwk, `",`, `", `, -1) jwk = jwk[0 : len(jwk)-1] jwk = jwk + `, "ext": true , "key_ops": ["sign"] }` // `{ "kty": "EC" , "crv": %q , "d": %q , "x": %q , "y": %q }`, jwk.Crv, jwk.D, jwk.X, jwk.Y fmt.Fprintf(w, jwk) }) http.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { baseURL := getBaseURL(r) log.Printf("%s %s\n", r.Method, r.URL.Path) fmt.Fprintf(w, `{ "issuer": "%s", "jwks_uri": "%s/.well-known/jwks.json" }`, baseURL, baseURL) }) http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s %s", r.Method, r.Host, r.URL.Path) parts := strings.Split(r.Host, ".") kid := parts[0] b, err := ioutil.ReadFile(filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json")) if nil != err { //http.Error(w, "Not Found", http.StatusNotFound) exp := strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10) jwk := string(keypairs.MarshalJWKPublicKey(pubkey)) jwk = strings.Replace(jwk, `{"`, `{ "`, 1) jwk = strings.Replace(jwk, `",`, `" ,`, -1) jwk = jwk[0 : len(jwk)-1] jwk = jwk + fmt.Sprintf(`, "ext": true , "key_ops": ["verify"], "exp": %s }`, exp) // { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s } jwkstr := fmt.Sprintf(`{ "keys": [ %s ] }`, jwk) fmt.Println(jwkstr) fmt.Fprintf(w, jwkstr) return } tok := &PublicJWK{} err = json.Unmarshal(b, tok) if nil != err { // TODO delete the bad file? http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } jwkstr := fmt.Sprintf( `{ "keys": [ { "kty": "EC", "crv": %q, "x": %q, "y": %q, "kid": %q,`+ ` "ext": true, "key_ops": ["verify"], "exp": %s } ] }`, tok.Crv, tok.X, tok.Y, tok.KeyID, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10), ) fmt.Println(jwkstr) fmt.Fprintf(w, jwkstr) }) return http.DefaultServeMux } type OTP struct { //Attempts int `json:"attempts"` CreatedAt time.Time `json:"created_at"` Email string `json:"email"` ReceiptUA string `json:"receipt_agent"` ReceiptIP string `json:"receipt_addr"` ReceiptUsed time.Time `json:"receipt_used"` SecretUA string `json:"secret_agent"` SecretIP string `json:"secret_addr"` SecretUsed time.Time `json:"secret_used"` } func hashOTPSecret(secret []byte) (hash string, receipt string) { tokenID := sha1.Sum(secret[:]) receipt = base64.RawURLEncoding.EncodeToString(tokenID[:]) return hashOTPReceipt(receipt), receipt } func hashOTPReceipt(receipt string) (hash string) { return base64.RawURLEncoding.EncodeToString([]byte(receipt)) } func newOTP(email string, agent string, addr string) (id string, secret []byte, err error) { uuid, err := uuid.NewRandom() secret, _ = uuid.MarshalBinary() if nil != err { // nothing else to do if we run out of random // or are on a platform that doesn't support random panic(fmt.Errorf("random bytes read failure: %w", err)) } // The (double) hash becomes the file or DB id. // Using this rather than the secret itself prevents DB / FS / compare timing attacks hash, receipt := hashOTPSecret(secret) otp := OTP{ CreatedAt: time.Now(), Email: strings.ToLower(email), ReceiptUA: agent, ReceiptIP: addr, //Attempts: 0, } otpJSON, _ := json.Marshal(otp) if err := ioutil.WriteFile( filepath.Join(tokenPrefix, hash+".tok.txt"), otpJSON, // keep it secret, keep it safe os.FileMode(0600), ); nil != err { return "", nil, errors.New("database connection failed when writing verification token") } return receipt, secret, nil } type otpConsumer struct { secret bool receipt bool } func consumeOTPSecret(email string, secret []byte, agent string, addr string) (*OTP, error) { hash, _ := hashOTPSecret(secret) return checkOTP(hash, email, agent, addr, otpConsumer{secret: true}) } func consumeOTPReceipt(email string, receipt string, agent string, addr string) (*OTP, error) { hash := hashOTPReceipt(receipt) return checkOTP(hash, email, agent, addr, otpConsumer{receipt: true}) } func checkOTP(hash, email, agent, addr string, consume otpConsumer) (*OTP, error) { email = strings.ToLower(email) // the double hash will not leak timing info on lookup tokfile := filepath.Join(tokenPrefix, hash+".tok.txt") b, err := ioutil.ReadFile(tokfile) if nil != err { return nil, errors.New("database connection failed when reading verification token") } otp := OTP{} if err := json.Unmarshal(b, &otp); nil != err { return nil, errors.New("database verification token parse failed") } if 0 == subtle.ConstantTimeCompare([]byte(otp.Email), []byte(email)) { // TODO error // TODO increment attempts? } if consume.secret { if !otp.SecretUsed.IsZero() { return nil, errors.New("token has already been used") } otp.SecretUsed = time.Now() if addr != otp.ReceiptIP { otp.SecretUA = agent otp.SecretIP = addr } if consume.receipt && otp.ReceiptUsed.IsZero() { otp.ReceiptUsed = otp.SecretUsed } } else if consume.receipt { if otp.SecretUsed.IsZero() { return nil, errors.New("token has not been verified") } if !otp.ReceiptUsed.IsZero() { return nil, errors.New("token has already been used") } otp.ReceiptUsed = time.Now() } if consume.secret || consume.receipt { otpJSON, _ := json.Marshal(otp) if err := ioutil.WriteFile( filepath.Join(tokenPrefix, hash+".tok.txt"), otpJSON, // keep it secret, keep it safe os.FileMode(0600), ); nil != err { return nil, errors.New("database connection failed when consuming token") } } fmt.Println("SNTHSNTHSNTHSNTHSNTHSNTHSNTHSNTHNSTHSNTH GOOD!!") return &otp, nil } func startVerification(baseURL, contact, agent, addr string) (receipt string, err error) { email := strings.Replace(strings.TrimPrefix(contact, "mailto:"), " ", "+", -1) if "" == email { return "", errors.New("missing contact:[\"mailto:me@example.com\"]") } // TODO check DNS for MX records if !strings.Contains(email, "@") || strings.Contains(email, " \t\n") { return "", errors.New("invalid email address") } // TODO expect JWK in JWS/JWT // TODO place validated JWK into file with token ua := ua.Parse(agent) receipt, secret, err := newOTP(email, agent, addr) if nil != err { return "", err } subject := "Verify New Account" // TODO go tpl // TODO determine OS and Browser from user agent text := fmt.Sprintf( "It looks like you just tried to register a new Pocket ID account.\n\n Verify account: %s#/verify/%s\n\n%s on %s %s from %s\n\nNot you? Just ignore this message.", baseURL, base64.RawURLEncoding.EncodeToString(secret), ua.Name, ua.OS, ua.Device, addr, ) fmt.Println("email:", text) if !strings.Contains(contact, "+noreply") { if _, err = SendSimpleMessage(email, defaultFrom, subject, text, defaultReplyTo); nil != err { return "", err } } return receipt, nil } func getBaseURL(r *http.Request) string { var scheme string if nil != r.TLS || "https" == r.Header.Get("X-Forwarded-Proto") { scheme = "https:" } else { scheme = "http:" } return fmt.Sprintf( "%s//%s", scheme, r.Host, ) } // HTTPError describes an error that should be propagated to the HTTP client type HTTPError struct { message string code int } func postEC(jwksPrefix string, tok map[string]interface{}, w http.ResponseWriter, r *http.Request) { data, err := postECHelper(r.Host, jwksPrefix, tok, r) if nil != err { http.Error(w, err.message, err.code) return } w.Write(data) } func postECHelper(hostname, jwksPrefix string, tok map[string]interface{}, r *http.Request) ([]byte, *HTTPError) { crv, ok := tok["crv"].(string) if 5 != len(crv) || "P-" != crv[:2] { return nil, &HTTPError{"Bad Request: bad curve", http.StatusBadRequest} } x, ok := tok["x"].(string) if !ok { return nil, &HTTPError{"Bad Request: missing 'x'", http.StatusBadRequest} } y, ok := tok["y"].(string) if !ok { return nil, &HTTPError{"Bad Request: missing 'y'", http.StatusBadRequest} } thumbprintable := []byte( fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, crv, x, y), ) alg := crv[2:] var thumb []byte switch alg { case "256": hash := sha256.Sum256(thumbprintable) thumb = hash[:] case "384": hash := sha512.Sum384(thumbprintable) thumb = hash[:] case "521": fallthrough case "512": hash := sha512.Sum512(thumbprintable) thumb = hash[:] default: return nil, &HTTPError{"Bad Request: bad key length or curve", http.StatusBadRequest} } kid := base64.RawURLEncoding.EncodeToString(thumb) if kid2, _ := tok["kid"].(string); "" != kid2 && kid != kid2 { return nil, &HTTPError{"Bad Request: kid should be " + kid, http.StatusBadRequest} } pub := []byte(fmt.Sprintf( `{"crv":%q,"kid":%q,"kty":"EC","x":%q,"y":%q}`, crv, kid, x, y, )) // TODO allow posting at the top-level? // TODO support a group of keys by PPID // (right now it's only by KID) if !strings.HasPrefix(hostname, strings.ToLower(kid)+".") { return nil, &HTTPError{"Bad Request: prefix should be " + kid, http.StatusBadRequest} } if err := ioutil.WriteFile( filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json"), pub, 0644, ); nil != err { fmt.Println("can't write file") return nil, &HTTPError{"Internal Server Error", http.StatusInternalServerError} } baseURL := getBaseURL(r) return []byte(fmt.Sprintf( `{ "iss":%q, "jwks_url":%q }`, baseURL+"/", baseURL+"/.well-known/jwks.json", )), nil } func postRSA(jwksPrefix string, tok map[string]interface{}, w http.ResponseWriter, r *http.Request) { data, err := postRSAHelper(r.Host, jwksPrefix, tok, r) if nil != err { http.Error(w, err.message, err.code) return } w.Write(data) } func postRSAHelper(hostname, jwksPrefix string, tok map[string]interface{}, r *http.Request) ([]byte, *HTTPError) { e, ok := tok["e"].(string) if !ok { return nil, &HTTPError{"Bad Request: missing 'e'", http.StatusBadRequest} } n, ok := tok["n"].(string) if !ok { return nil, &HTTPError{"Bad Request: missing 'n'", http.StatusBadRequest} } thumbprintable := []byte( fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, e, n), ) var thumb []byte // TODO handle bit lengths well switch 3 * (len(n) / 4.0) { case 256: hash := sha256.Sum256(thumbprintable) thumb = hash[:] case 384: hash := sha512.Sum384(thumbprintable) thumb = hash[:] case 512: hash := sha512.Sum512(thumbprintable) thumb = hash[:] default: return nil, &HTTPError{"Bad Request: only standard RSA key lengths (2048, 3072, 4096) are supported", http.StatusBadRequest} } kid := base64.RawURLEncoding.EncodeToString(thumb) if kid2, _ := tok["kid"].(string); "" != kid2 && kid != kid2 { return nil, &HTTPError{"Bad Request: kid should be " + kid, http.StatusBadRequest} } pub := []byte(fmt.Sprintf( `{"e":%q,"kid":%q,"kty":"EC","n":%q}`, e, kid, n, )) // TODO allow posting at the top-level? // TODO support a group of keys by PPID // (right now it's only by KID) if !strings.HasPrefix(hostname, strings.ToLower(kid)+".") { return nil, &HTTPError{"Bad Request: prefix should be " + kid, http.StatusBadRequest} } if err := ioutil.WriteFile( filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json"), pub, 0644, ); nil != err { fmt.Println("can't write file") return nil, &HTTPError{"Internal Server Error", http.StatusInternalServerError} } baseURL := getBaseURL(r) return []byte(fmt.Sprintf( `{ "iss":%q, "jwks_url":%q }`, baseURL+"/", baseURL+"/.well-known/jwks.json", )), nil }