2020-07-25 09:13:19 +00:00
package mockid
import (
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
2020-09-13 05:55:12 +00:00
"crypto/subtle"
2020-07-25 09:13:19 +00:00
"encoding/base64"
"encoding/json"
2020-08-17 23:14:09 +00:00
"errors"
2020-07-25 09:13:19 +00:00
"fmt"
"io/ioutil"
"log"
2020-09-17 07:28:55 +00:00
"net"
2020-07-25 09:13:19 +00:00
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
2020-09-13 05:55:12 +00:00
"git.coolaj86.com/coolaj86/go-mockid/kvdb"
2020-08-01 23:59:20 +00:00
"git.coolaj86.com/coolaj86/go-mockid/mockid/api"
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
2020-07-25 09:13:19 +00:00
"git.rootprojects.org/root/keypairs"
"git.rootprojects.org/root/keypairs/keyfetch"
2020-09-13 05:55:12 +00:00
2020-07-25 09:13:19 +00:00
"github.com/google/uuid"
2020-09-13 05:55:12 +00:00
ua "github.com/mileusna/useragent"
2020-07-25 09:13:19 +00:00
)
2020-09-17 07:28:55 +00:00
var errTokenNotVerified = apiError { "token has not been verified" }
var errUsedToken = apiError { "token has already been used" }
var errInvalidEmail = apiError { "invalid email address" }
// API Errors
type serverError struct {
error string
}
func ( e serverError ) Error ( ) string {
return e . error
}
type apiError struct {
error string
}
func ( e apiError ) Error ( ) string {
return e . error
}
2020-08-17 23:14:09 +00:00
type HTTPResponse struct {
2020-09-17 07:28:55 +00:00
Error string ` json:"error,omitempty" `
Code string ` json:"code,omitempty" `
2020-08-17 23:14:09 +00:00
Success bool ` json:"success" `
}
2020-09-13 05:55:12 +00:00
type TokenResponse struct {
Receipt string ` json:"receipt" `
HTTPResponse
}
type OTPResponse struct {
OTP
HTTPResponse
}
2020-09-16 22:37:19 +00:00
type Contact struct {
Email string ` json:"email" `
Subjects [ ] string ` json:"subjects" `
}
type Subject struct {
Subject string ` json:"subject" `
Emails map [ string ] time . Time ` json:"emails" `
}
var privKeyJWKPath string
2020-09-13 05:55:12 +00:00
var tokenPrefix string
2020-08-01 23:59:20 +00:00
// Route returns an HTTP Mux containing the full API
2020-07-25 09:13:19 +00:00
func Route ( jwksPrefix string , privkey keypairs . PrivateKey ) http . Handler {
Init ( )
2020-09-16 22:37:19 +00:00
contactPrefix := jwksPrefix + "/contacts"
2020-09-13 05:55:12 +00:00
contactKV := kvdb . KVDB {
2020-09-16 22:37:19 +00:00
Prefix : contactPrefix ,
2020-09-13 05:55:12 +00:00
Ext : "eml.json" ,
}
2020-09-16 22:37:19 +00:00
subjectPrefix := jwksPrefix + "/subjects"
subjectKV := kvdb . KVDB {
Prefix : subjectPrefix ,
Ext : "sub.json" ,
}
2020-07-25 09:13:19 +00:00
// TODO get from main()
2020-09-13 05:55:12 +00:00
tokenPrefix = jwksPrefix + "/tokens"
2020-09-16 22:37:19 +00:00
for _ , pre := range [ ] string { tokenPrefix , contactPrefix , subjectPrefix } {
2020-09-13 05:55:12 +00:00
if err := os . MkdirAll ( pre , 0750 ) ; nil != err {
panic ( err )
}
}
2020-07-25 09:13:19 +00:00
pubkey := keypairs . NewPublicKey ( privkey . Public ( ) )
2020-09-16 22:37:19 +00:00
privKeyJWKPath = jwksPrefix + "/private/priv.jwk.json"
signingKey , err := xkeypairs . ParsePrivateKeyFile ( privKeyJWKPath )
if nil != err {
signingKey = xkeypairs . GenPrivKey ( & xkeypairs . KeyOptions { } )
_ = os . MkdirAll ( jwksPrefix + "/private" , 0750 )
b := xkeypairs . MarshalJWKPrivateKey ( signingKey )
if err := ioutil . WriteFile ( privKeyJWKPath , b , 0600 ) ; nil != err {
panic ( err )
}
}
signingPub := keypairs . NewPublicKey ( signingKey . Public ( ) )
//signingKid := keypairs.ThumbprintPublicKey(signingPub)
2020-08-17 23:14:09 +00:00
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" )
2020-09-13 05:55:12 +00:00
// add reasonable security options
2020-08-17 23:14:09 +00:00
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 )
} )
2020-09-13 05:55:12 +00:00
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 {
2020-09-16 22:37:19 +00:00
fmt . Println ( "meta: empty contact query" )
2020-09-13 05:55:12 +00:00
http . Error ( w , "Bad Request" , http . StatusBadRequest )
b , _ := json . Marshal ( & HTTPResponse {
Error : "missing require query parameter 'contact'" ,
2020-09-17 07:59:38 +00:00
Code : "E_USER" ,
2020-09-13 05:55:12 +00:00
} )
w . Write ( b )
return
}
2020-09-17 07:28:55 +00:00
contact , err = lintEmail ( contact )
if nil != err {
b , _ := json . Marshal ( & HTTPResponse {
Error : err . Error ( ) ,
Code : "E_USER" ,
} )
w . Write ( b )
return
}
2020-09-13 05:55:12 +00:00
_ , ok , err := contactKV . Load ( contact )
if nil != err {
2020-09-16 22:37:19 +00:00
fmt . Fprintf ( os . Stderr , "meta: error loading contact: %s\n" , err . Error ( ) )
2020-09-13 05:55:12 +00:00
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" ,
2020-09-17 07:28:55 +00:00
Code : "E_USER" ,
2020-09-13 05:55:12 +00:00
} )
w . Write ( b )
} )
2020-09-16 22:37:19 +00:00
// TODO rename as challenge?
2020-09-13 05:55:12 +00:00
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 )
} )
2020-09-16 22:37:19 +00:00
// TODO rename as present?
2020-09-13 05:55:12 +00:00
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 {
2020-09-17 07:28:55 +00:00
if errTokenNotVerified == err {
b , _ := json . Marshal ( & HTTPResponse {
Error : err . Error ( ) ,
2020-09-17 07:59:38 +00:00
Code : "E_RETRY" ,
2020-09-17 07:28:55 +00:00
} )
w . Write ( b )
return
}
2020-09-13 05:55:12 +00:00
// TODO propagate error types
http . Error ( w , "Bad Request" , http . StatusBadRequest )
fmt . Fprintf ( w , "%s" , err )
return
}
2020-09-16 22:37:19 +00:00
var contact * Contact
val , ok , err := contactKV . Load ( otp . Email , & Contact { } )
if nil != err {
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
fmt . Println ( "error loading contact:" , err )
fmt . Fprintf ( w , "%s" , err )
return
}
if ok {
contact = val . ( * Contact )
} else {
// TODO c := Contacts.New(); c.Save();
uuid , _ := uuid . NewRandom ( )
uuidBin , _ := uuid . MarshalBinary ( )
sub := base64 . RawURLEncoding . EncodeToString ( uuidBin )
contact = & Contact {
Email : otp . Email ,
Subjects : [ ] string { sub } ,
}
if err := contactKV . Store ( otp . Email , contact ) ; nil != err {
fmt . Println ( "ERROR contactKV Store" , err )
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
fmt . Fprintf ( w , "%s" , err )
return
}
subject := & Subject {
Subject : sub ,
Emails : map [ string ] time . Time {
otp . Email : time . Now ( ) ,
} ,
}
if err := subjectKV . Store ( sub , subject ) ; nil != err {
fmt . Println ( "ERROR subjectKV Store" , err )
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
fmt . Fprintf ( w , "%s" , err )
return
}
}
var sub string
if len ( contact . Subjects ) < 1 || len ( contact . Subjects ) > 1 {
fmt . Printf ( "%#v\n" , contact )
if len ( contact . Subjects ) > 1 {
fmt . Fprintf ( os . Stderr , "TODO: give exchange token for multiple accounts\n" )
}
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
fmt . Fprintf ( w , "%s" , err )
return
}
sub = contact . Subjects [ 0 ]
uuid , _ := uuid . NewRandom ( )
nonce , _ := uuid . MarshalBinary ( )
baseURL := getBaseURL ( r )
tok , err := xkeypairs . SignClaims (
signingKey ,
xkeypairs . Object { } ,
xkeypairs . Object {
"sub" : sub ,
2020-09-16 23:19:38 +00:00
"iss" : baseURL + "/" ,
2020-09-16 22:37:19 +00:00
"jti" : base64 . RawURLEncoding . EncodeToString ( nonce ) ,
"iat" : time . Now ( ) . Unix ( ) ,
"exp" : "1h" ,
} ,
)
if nil != err {
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
fmt . Fprintf ( w , "%s" , err )
return
}
otp . AccessToken = xkeypairs . JWSToJWT ( tok )
2020-09-13 05:55:12 +00:00
b , _ := json . Marshal ( & OTPResponse {
HTTPResponse : HTTPResponse { Success : true } ,
OTP : * otp ,
} )
w . Write ( b )
} )
2020-09-16 23:19:38 +00:00
http . HandleFunc ( "/api/authz/email" , 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 {
http . Error (
w ,
"Bad Format: missing Authorization header and 'access_token' query" ,
http . StatusBadRequest ,
)
return
}
parts := strings . Split ( token , " " )
if 2 != len ( parts ) {
http . Error (
w ,
"Bad Format: expected Authorization header to be in the format of 'Bearer <Token>'" ,
http . StatusBadRequest ,
)
return
}
token = parts [ 1 ]
inspected , err := verifyToken ( token )
if nil != err {
http . Error ( w , "Invalid Token: " + err . Error ( ) , http . StatusBadRequest )
return
}
if ! inspected . Verified {
http . Error ( w , "bad token signature" , http . StatusBadRequest )
return
}
iss , _ := inspected . Payload [ "iss" ] . ( string )
baseURL := getBaseURL ( r )
if baseURL != strings . TrimSuffix ( iss , "/" ) {
http . Error ( w , "unacceptable token issuer" , http . StatusBadRequest )
return
}
subject , _ := inspected . Payload [ "sub" ] . ( string )
value , ok , err := subjectKV . Load ( subject , & Subject { } )
if nil != err {
http . Error ( w , "Server Error: " + err . Error ( ) ,
http . StatusInternalServerError ,
)
return
}
if ! ok {
http . Error ( w , "Server Error: the user went missing... huh" ,
http . StatusInternalServerError ,
)
return
}
sub , ok := value . ( * Subject )
if ! ok {
http . Error (
w ,
"Server Error: invalid data type" ,
http . StatusInternalServerError ,
)
return
}
var emails [ ] string
for k := range sub . Emails {
emails = append ( emails , k )
}
jsonb , _ := json . MarshalIndent ( struct {
Emails [ ] string ` json:"emails" `
} {
Emails : emails ,
} , "" , " " )
fmt . Fprintf ( w , string ( jsonb ) + "\n" )
} )
2020-08-17 23:14:09 +00:00
http . HandleFunc ( "/api/new-nonce" , requireHashcash ( func ( w http . ResponseWriter , r * http . Request ) {
indexURL := getBaseURL ( r ) + "/api/directory"
2020-08-01 23:59:20 +00:00
w . Header ( ) . Set ( "Link" , "<" + indexURL + ">;rel=\"index\"" )
2020-08-17 23:14:09 +00:00
w . Header ( ) . Set ( "Date" , time . Now ( ) . Format ( http . TimeFormat ) )
// disable caching in every possible way
w . Header ( ) . Set ( "Expires" , time . Now ( ) . Format ( http . TimeFormat ) )
2020-07-25 09:13:19 +00:00
w . Header ( ) . Set ( "Cache-Control" , "max-age=0, no-cache, no-store" )
w . Header ( ) . Set ( "Pragma" , "no-cache" )
2020-08-17 23:14:09 +00:00
w . Header ( ) . Set ( "Strict-Transport-Security" , "max-age=604800" )
2020-07-25 09:13:19 +00:00
w . Header ( ) . Set ( "X-Frame-Options" , "DENY" )
2020-08-17 23:14:09 +00:00
2020-07-25 09:13:19 +00:00
issueNonce ( w , r )
2020-08-17 23:14:09 +00:00
} ) )
http . HandleFunc ( "/api/test-hashcash" , func ( w http . ResponseWriter , r * http . Request ) {
2020-09-13 05:55:12 +00:00
if err := UseHashcash ( r . Header . Get ( "Hashcash" ) , r . Host ) ; nil != err {
2020-08-17 23:14:09 +00:00
b , _ := json . Marshal ( & HTTPResponse {
Error : err . Error ( ) ,
} )
w . Write ( b )
return
}
b , _ := json . Marshal ( & HTTPResponse {
Success : true ,
} )
w . Write ( b )
2020-07-25 09:13:19 +00:00
} )
2020-08-17 23:14:09 +00:00
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 )
2020-07-25 09:13:19 +00:00
return
}
2020-08-17 23:14:09 +00:00
defer r . Body . Close ( )
2020-07-25 09:13:19 +00:00
2020-08-17 23:14:09 +00:00
if err := jws . DecodeComponents ( ) ; nil != err {
http . Error ( w , "Bad Request" , http . StatusBadRequest )
fmt . Fprintf ( w , ` { "error":%q} ` + "\n" , err )
2020-07-25 09:13:19 +00:00
return
}
2020-08-17 23:14:09 +00:00
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
}
2020-07-25 09:13:19 +00:00
2020-08-17 23:14:09 +00:00
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" )
2020-07-25 09:13:19 +00:00
return
}
2020-08-17 23:14:09 +00:00
contacts , _ := jws . Claims [ "contact" ] . ( [ ] string )
contact := ""
if 1 == len ( contacts ) {
contact = contacts [ 0 ]
}
2020-07-25 09:13:19 +00:00
baseURL := getBaseURL ( r )
2020-09-13 05:55:12 +00:00
addr := strings . Split ( r . RemoteAddr , ":" ) [ 0 ]
receipt , err := startVerification (
baseURL ,
contact ,
r . Header . Get ( "User-Agent" ) ,
addr ,
)
if nil != err {
2020-08-17 23:14:09 +00:00
http . Error ( w , "Bad Request" , http . StatusBadRequest )
msg , _ := json . Marshal ( err . Error ( ) )
fmt . Fprintf ( w , ` { "error":%s} ` + "\n" , msg )
2020-07-25 09:13:19 +00:00
return
}
2020-09-13 05:55:12 +00:00
fmt . Fprintf ( w , ` { "success": true, "error": "", "receipt":, "%s" }%s ` , receipt , "\n" )
2020-08-17 23:14:09 +00:00
} )
2020-07-25 09:13:19 +00:00
// 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
}
2020-09-13 05:55:12 +00:00
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 {
2020-08-17 23:14:09 +00:00
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
fmt . Fprintf ( w , "%s" , err )
2020-07-25 09:13:19 +00:00
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 )
} )
2020-08-04 07:09:43 +00:00
// TODO add /debug prefix
2020-08-10 21:47:12 +00:00
http . HandleFunc ( "/debug/private.jwk.json" , api . GeneratePrivateJWK )
http . HandleFunc ( "/debug/priv.der" , api . GeneratePrivateDER )
http . HandleFunc ( "/debug/priv.pem" , api . GeneratePrivatePEM )
2020-07-25 09:13:19 +00:00
2020-08-10 21:47:12 +00:00
http . HandleFunc ( "/debug/public.jwk.json" , api . GeneratePublicJWK )
http . HandleFunc ( "/debug/pub.der" , api . GeneratePublicDER )
http . HandleFunc ( "/debug/pub.pem" , api . GeneratePublicPEM )
2020-08-02 09:39:56 +00:00
2020-08-10 21:47:12 +00:00
http . HandleFunc ( "/debug/jose.jws.json" , api . SignJWS )
http . HandleFunc ( "/debug/jose.jws.jwt" , api . SignJWT )
http . HandleFunc ( "/debug/verify" , api . Verify )
2020-08-04 07:09:43 +00:00
2020-07-25 09:13:19 +00:00
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 {
2020-09-16 23:19:38 +00:00
http . Error (
w ,
"Bad Format: missing Authorization header and 'access_token' query" ,
http . StatusBadRequest ,
)
2020-07-25 09:13:19 +00:00
return
}
} else {
parts := strings . Split ( token , " " )
if 2 != len ( parts ) {
2020-09-16 23:19:38 +00:00
http . Error (
w ,
"Bad Format: expected Authorization header to be in the format of 'Bearer <Token>'" ,
http . StatusBadRequest ,
)
2020-07-25 09:13:19 +00:00
return
}
token = parts [ 1 ]
}
2020-09-16 23:19:38 +00:00
inspected , err := verifyToken ( token )
2020-07-25 09:13:19 +00:00
if nil != err {
2020-09-16 23:19:38 +00:00
http . Error ( w , "Invalid Token: " + err . Error ( ) , http . StatusBadRequest )
2020-07-25 09:13:19 +00:00
return
}
tokenB , _ := json . MarshalIndent ( inspected , "" , " " )
if nil != err {
2020-09-16 23:19:38 +00:00
http . Error (
w ,
"Bad Format: malformed token, or malformed jwk at issuer url" ,
http . StatusInternalServerError ,
)
2020-07-25 09:13:19 +00:00
return
}
2020-09-16 22:37:19 +00:00
fmt . Fprintf ( w , string ( tokenB ) + "\n" )
2020-07-25 09:13:19 +00:00
} )
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 )
2020-08-01 23:59:20 +00:00
jwk := string ( xkeypairs . MarshalJWKPrivateKey ( privkey ) )
2020-07-25 09:13:19 +00:00
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 )
2020-09-16 22:37:19 +00:00
jwkReal := string ( keypairs . MarshalJWKPublicKey ( signingPub ) )
jwkReal = strings . Replace ( jwkReal , ` { " ` , ` { " ` , 1 )
jwkReal = strings . Replace ( jwkReal , ` ", ` , ` " , ` , - 1 )
jwkReal = jwkReal [ 0 : len ( jwkReal ) - 1 ]
jwkReal = jwkReal + fmt . Sprintf ( ` , "ext": true , "key_ops": ["verify"], "exp": %s, "REAL": true } ` , exp )
jwkFake := string ( keypairs . MarshalJWKPublicKey ( pubkey ) )
jwkFake = strings . Replace ( jwkFake , ` { " ` , ` { " ` , 1 )
jwkFake = strings . Replace ( jwkFake , ` ", ` , ` " , ` , - 1 )
jwkFake = jwkFake [ 0 : len ( jwkFake ) - 1 ]
jwkFake = jwkFake + fmt . Sprintf ( ` , "ext": true , "key_ops": ["verify"], "exp": %s, "FAKE": true } ` , exp )
2020-07-25 09:13:19 +00:00
// { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s }
2020-09-16 22:37:19 +00:00
jwkstr := fmt . Sprintf ( ` { "keys": [ %s, %s ] } ` , jwkFake , jwkReal )
fmt . Println ( "jwkstr:" , jwkstr )
2020-07-25 09:13:19 +00:00
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 ) ,
)
2020-09-16 22:37:19 +00:00
fmt . Println ( "jwkstr:" , jwkstr )
2020-07-25 09:13:19 +00:00
fmt . Fprintf ( w , jwkstr )
} )
return http . DefaultServeMux
}
2020-09-16 23:19:38 +00:00
func verifyToken ( token string ) ( * InspectableToken , error ) {
parts := strings . Split ( token , "." )
if 3 != len ( parts ) {
return nil , errors . New ( "Bad Format: token should be in the format of <protected-header>.<payload>.<signature>" )
}
protected64 := parts [ 0 ]
payload64 := parts [ 1 ]
signature64 := parts [ 2 ]
protectedB , err := base64 . RawURLEncoding . DecodeString ( protected64 )
if nil != err {
return nil , errors . New ( "Bad Format: token's header should be URL-safe base64 encoded" )
}
payloadB , err := base64 . RawURLEncoding . DecodeString ( payload64 )
if nil != err {
return nil , errors . New ( "Bad Format: token's payload should be URL-safe base64 encoded" )
}
// TODO verify signature
sig , err := base64 . RawURLEncoding . DecodeString ( signature64 )
if nil != err {
return nil , errors . New ( "Bad Format: token's signature should be URL-safe base64 encoded" )
}
errs := [ ] string { }
protected := map [ string ] interface { } { }
err = json . Unmarshal ( protectedB , & protected )
if nil != err {
return nil , errors . New ( "Bad Format: token's header should be URL-safe base64-encoded JSON" )
}
kid , kidOK := protected [ "kid" ] . ( string )
// TODO parse jwkM
_ , jwkOK := protected [ "jwk" ]
if ! kidOK && ! jwkOK {
errs = append ( errs , "must have either header.kid or header.jwk" )
}
data := map [ string ] interface { } { }
err = json . Unmarshal ( payloadB , & data )
if nil != err {
return nil , errors . New ( "Bad Format: token's payload should be URL-safe base64-encoded JSON" )
}
iss , issOK := data [ "iss" ] . ( string )
if ! jwkOK && ! issOK {
errs = append ( errs , "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 : errs ,
}
return inspected , nil
}
2020-09-13 05:55:12 +00:00
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" `
2020-09-16 22:37:19 +00:00
AccessToken string ` json:"access_token,omitempty" `
2020-09-13 05:55:12 +00:00
}
2020-08-17 23:14:09 +00:00
2020-09-13 05:55:12 +00:00
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 ( )
2020-08-17 23:14:09 +00:00
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 ) )
}
2020-09-13 05:55:12 +00:00
// 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 )
2020-08-17 23:14:09 +00:00
if err := ioutil . WriteFile (
2020-09-13 05:55:12 +00:00
filepath . Join ( tokenPrefix , hash + ".tok.txt" ) ,
otpJSON ,
2020-08-17 23:14:09 +00:00
// keep it secret, keep it safe
os . FileMode ( 0600 ) ,
) ; nil != err {
2020-09-17 07:28:55 +00:00
return "" , nil , serverError { "database connection failed when writing verification token" }
2020-08-17 23:14:09 +00:00
}
2020-09-13 05:55:12 +00:00
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 } )
2020-08-17 23:14:09 +00:00
}
2020-09-13 05:55:12 +00:00
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 {
2020-09-17 07:28:55 +00:00
return nil , serverError { "database connection failed when reading verification token" }
2020-09-13 05:55:12 +00:00
}
otp := OTP { }
if err := json . Unmarshal ( b , & otp ) ; nil != err {
2020-09-17 07:28:55 +00:00
return nil , serverError { "database verification token parse failed" }
2020-09-13 05:55:12 +00:00
}
if 0 == subtle . ConstantTimeCompare ( [ ] byte ( otp . Email ) , [ ] byte ( email ) ) {
// TODO error
// TODO increment attempts?
2020-08-17 23:14:09 +00:00
}
2020-09-13 05:55:12 +00:00
if consume . secret {
if ! otp . SecretUsed . IsZero ( ) {
2020-09-17 07:28:55 +00:00
return nil , errUsedToken
2020-09-13 05:55:12 +00:00
}
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 ( ) {
2020-09-17 07:28:55 +00:00
return nil , errTokenNotVerified
2020-09-13 05:55:12 +00:00
}
if ! otp . ReceiptUsed . IsZero ( ) {
2020-09-17 07:28:55 +00:00
return nil , errUsedToken
2020-09-13 05:55:12 +00:00
}
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 {
2020-09-17 07:28:55 +00:00
return nil , serverError { "database connection failed when consuming token" }
2020-09-13 05:55:12 +00:00
}
}
return & otp , nil
2020-08-17 23:14:09 +00:00
}
2020-09-17 07:28:55 +00:00
func lintEmail ( email string ) ( string , error ) {
// TODO check DNS for MX records
parts := strings . Split ( email , "@" )
domain := parts [ 1 ]
if 2 != len ( parts ) || strings . Contains ( email , " \t\n" ) {
return "" , errInvalidEmail
}
mxs , err := net . LookupMX ( domain )
if len ( mxs ) < 1 || nil != err {
// TODO it possible in some cases that this
// could be a network error
return "" , errInvalidEmail
}
return strings . ToLower ( email ) , nil
}
2020-09-13 05:55:12 +00:00
func startVerification ( baseURL , contact , agent , addr string ) ( receipt string , err error ) {
email := strings . Replace ( strings . TrimPrefix ( contact , "mailto:" ) , " " , "+" , - 1 )
2020-08-17 23:14:09 +00:00
if "" == email {
2020-09-13 05:55:12 +00:00
return "" , errors . New ( "missing contact:[\"mailto:me@example.com\"]" )
2020-08-17 23:14:09 +00:00
}
2020-09-17 07:28:55 +00:00
email , err = lintEmail ( email )
if nil != err {
return "" , err
2020-08-17 23:14:09 +00:00
}
// TODO expect JWK in JWS/JWT
// TODO place validated JWK into file with token
2020-09-13 05:55:12 +00:00
ua := ua . Parse ( agent )
receipt , secret , err := newOTP ( email , agent , addr )
2020-08-17 23:14:09 +00:00
if nil != err {
2020-09-13 05:55:12 +00:00
return "" , err
2020-08-17 23:14:09 +00:00
}
subject := "Verify New Account"
// TODO go tpl
// TODO determine OS and Browser from user agent
2020-09-17 07:28:55 +00:00
page := "pocket/iframe.html"
2020-08-17 23:14:09 +00:00
text := fmt . Sprintf (
2020-09-17 07:28:55 +00:00
"It looks like you just tried to register a new Pocket ID account.\n\n Verify account: %s/%s#/verify/%s\n\n%s on %s %s from %s\n\nNot you? Just ignore this message." ,
baseURL , page , base64 . RawURLEncoding . EncodeToString ( secret ) , ua . Name , ua . OS , ua . Device , addr ,
2020-08-17 23:14:09 +00:00
)
2020-09-13 05:55:12 +00:00
fmt . Println ( "email:" , text )
if ! strings . Contains ( contact , "+noreply" ) {
if _ , err = SendSimpleMessage ( email , defaultFrom , subject , text , defaultReplyTo ) ; nil != err {
return "" , err
}
2020-08-17 23:14:09 +00:00
}
2020-09-13 05:55:12 +00:00
return receipt , nil
2020-08-17 23:14:09 +00:00
}
2020-07-25 09:13:19 +00:00
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 ,
)
}
2020-08-01 23:59:20 +00:00
// HTTPError describes an error that should be propagated to the HTTP client
2020-07-25 09:13:19 +00:00
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
}