identity management and use
This commit is contained in:
parent
230933ee0e
commit
a90327bcc0
30
actions.go
30
actions.go
|
@ -18,12 +18,6 @@ import (
|
|||
"tildegit.org/tjp/sliderule/gopher"
|
||||
)
|
||||
|
||||
var client sliderule.Client
|
||||
|
||||
func init() {
|
||||
client = sliderule.NewClient(nil)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrMustBeOnAPage = errors.New("you must be on a page to do that, use the \"go\" command first")
|
||||
ErrNoPreviousHistory = errors.New("there is no previous page in the history")
|
||||
|
@ -117,6 +111,8 @@ func Reload(state *BrowserState, conf *Config) error {
|
|||
urlStr, _ = gopherURL(state.Url)
|
||||
}
|
||||
|
||||
var client = sliderule.NewClient(tlsConfig(state))
|
||||
|
||||
var response *sliderule.Response
|
||||
var err error
|
||||
if state.Url.Scheme == "spartan" && state.Url.Fragment == "prompt" {
|
||||
|
@ -580,6 +576,28 @@ func TourCmd(state *BrowserState, args []string, conf *Config) error {
|
|||
return ErrInvalidTourArgs
|
||||
}
|
||||
|
||||
func IdentityCmd(state *BrowserState, args []string) error {
|
||||
switch args[0] {
|
||||
case "create":
|
||||
return IdentityCreate(state, args[1])
|
||||
case "list":
|
||||
return IdentityList(state)
|
||||
case "delete":
|
||||
return IdentityDelete(state, args[1])
|
||||
case "use":
|
||||
switch args[2] {
|
||||
case "domain":
|
||||
return IdentityUseDomain(state, args[1], args[3])
|
||||
case "folder":
|
||||
return IdentityUseFolder(state, args[1], args[3])
|
||||
case "page":
|
||||
return IdentityUsePage(state, args[1], args[3])
|
||||
}
|
||||
}
|
||||
|
||||
return ErrInvalidArgs
|
||||
}
|
||||
|
||||
func Pipe(state *BrowserState, cmdStr string) error {
|
||||
if state.Body == nil {
|
||||
return ErrMustBeOnAPage
|
||||
|
|
79
command.go
79
command.go
|
@ -73,6 +73,14 @@ func ParseCommand(line string) (*Command, error) {
|
|||
|
||||
}
|
||||
}
|
||||
case 'i':
|
||||
if strings.HasPrefix("identity", cmd) {
|
||||
args, err := parseIdentityArgs(rest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Command{Name: "identity", Args: args}, nil
|
||||
}
|
||||
case 'n':
|
||||
if strings.HasPrefix("next", cmd) {
|
||||
return &Command{Name: "next"}, nil
|
||||
|
@ -157,11 +165,11 @@ func ParseCommand(line string) (*Command, error) {
|
|||
}
|
||||
|
||||
func parseMarkArgs(line string) ([]string, error) {
|
||||
if line == "" {
|
||||
return nil, ErrInvalidArgs
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
return []string{"list"}, nil
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
switch fields[0][0] {
|
||||
case 'a':
|
||||
if strings.HasPrefix("add", fields[0]) {
|
||||
|
@ -291,6 +299,69 @@ func parseTourArgs(line string) ([]string, error) {
|
|||
return append([]string{"add"}, fields...), nil
|
||||
}
|
||||
|
||||
func parseIdentityArgs(line string) ([]string, error) {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
return []string{"list"}, nil
|
||||
}
|
||||
|
||||
switch fields[0][0] {
|
||||
case 'c':
|
||||
if strings.HasPrefix("create", fields[0]) {
|
||||
fields[0] = "create"
|
||||
if len(fields) != 2 {
|
||||
return nil, ErrInvalidArgs
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
case 'l':
|
||||
if strings.HasPrefix("list", fields[0]) {
|
||||
if len(fields) != 1 {
|
||||
return nil, ErrInvalidArgs
|
||||
}
|
||||
return []string{"list"}, nil
|
||||
}
|
||||
case 'd':
|
||||
if strings.HasPrefix("delete", fields[0]) {
|
||||
fields[0] = "delete"
|
||||
if len(fields) != 2 {
|
||||
return nil, ErrInvalidArgs
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
case 'u':
|
||||
if strings.HasPrefix("use", fields[0]) {
|
||||
fields[0] = "use"
|
||||
if len(fields) != 4 {
|
||||
return nil, ErrInvalidArgs
|
||||
}
|
||||
switch fields[2][0] {
|
||||
case 'd':
|
||||
if !strings.HasPrefix("domain", fields[2]) {
|
||||
return nil, ErrInvalidArgs
|
||||
}
|
||||
fields[2] = "domain"
|
||||
case 'f':
|
||||
if !strings.HasPrefix("folder", fields[2]) {
|
||||
return nil, ErrInvalidArgs
|
||||
}
|
||||
fields[2] = "folder"
|
||||
case 'p':
|
||||
if !strings.HasPrefix("page", fields[2]) {
|
||||
return nil, ErrInvalidArgs
|
||||
}
|
||||
fields[2] = "page"
|
||||
default:
|
||||
return nil, ErrInvalidArgs
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrInvalidArgs
|
||||
}
|
||||
|
||||
func RunCommand(conf *Config, cmd *Command, state *BrowserState) error {
|
||||
switch cmd.Name {
|
||||
case "about":
|
||||
|
@ -337,6 +408,8 @@ func RunCommand(conf *Config, cmd *Command, state *BrowserState) error {
|
|||
return Mark(state, cmd.Args, conf)
|
||||
case "tour":
|
||||
return TourCmd(state, cmd.Args, conf)
|
||||
case "identity":
|
||||
return IdentityCmd(state, cmd.Args)
|
||||
case "quit":
|
||||
os.Exit(0)
|
||||
}
|
||||
|
|
141
files.go
141
files.go
|
@ -2,6 +2,8 @@ package main
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
@ -264,3 +266,142 @@ func ensurePath(fpath string) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIdentities() (Identities, error) {
|
||||
idents := Identities{
|
||||
ByName: map[string]*tls.Config{},
|
||||
ByDomain: map[string]*tls.Config{},
|
||||
ByFolder: map[string]*tls.Config{},
|
||||
ByPage: map[string]*tls.Config{},
|
||||
}
|
||||
|
||||
manifest, err := dataFilePath("identities")
|
||||
if err != nil {
|
||||
return idents, err
|
||||
}
|
||||
|
||||
f, err := os.Open(manifest)
|
||||
if err != nil {
|
||||
return idents, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
var curident *tls.Config
|
||||
rdr := bufio.NewScanner(f)
|
||||
for rdr.Scan() {
|
||||
line := rdr.Text()
|
||||
if strings.HasPrefix(line, ":") {
|
||||
kind, location, _ := strings.Cut(line[1:], " ")
|
||||
switch kind {
|
||||
case "domain":
|
||||
idents.ByDomain[location] = curident
|
||||
case "folder":
|
||||
idents.ByFolder[location] = curident
|
||||
case "page":
|
||||
idents.ByPage[location] = curident
|
||||
}
|
||||
} else {
|
||||
name := strings.TrimSuffix(line, ":")
|
||||
curident, err = getIdentity(name)
|
||||
if err != nil {
|
||||
return idents, err
|
||||
}
|
||||
idents.ByName[name] = curident
|
||||
}
|
||||
}
|
||||
if err := rdr.Err(); err != nil {
|
||||
return idents, err
|
||||
}
|
||||
|
||||
return idents, nil
|
||||
}
|
||||
|
||||
func saveIdentities(idents Identities) error {
|
||||
manifest, err := dataFilePath("identities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(manifest, os.O_WRONLY|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
for name, ident := range idents.ByName {
|
||||
if _, err := fmt.Fprintf(f, "%s:\n", name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for domain, id := range idents.ByDomain {
|
||||
if id != ident {
|
||||
continue
|
||||
}
|
||||
if _, err := fmt.Fprintf(f, ":domain %s\n", domain); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for folder, id := range idents.ByFolder {
|
||||
if id != ident {
|
||||
continue
|
||||
}
|
||||
if _, err := fmt.Fprintf(f, ":folder %s\n", folder); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for page, id := range idents.ByPage {
|
||||
if id != ident {
|
||||
continue
|
||||
}
|
||||
if _, err := fmt.Fprintf(f, ":page %s\n", page); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIdentity(name string) (*tls.Config, error) {
|
||||
fpath, err := dataFilePath("ident/" + name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(fpath, fpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identityForCert(cert), nil
|
||||
}
|
||||
|
||||
func saveIdentity(name string, privkeyDER, certDER []byte) (string, error) {
|
||||
fpath, err := dataFilePath("ident/" + name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
if err := pem.Encode(f, &pem.Block{Type: "PRIVATE KEY", Bytes: privkeyDER}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fpath, nil
|
||||
}
|
||||
|
||||
func removeIdentity(name string) error {
|
||||
fpath, err := dataFilePath("ident/" + name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Remove(fpath)
|
||||
}
|
||||
|
|
34
help.go
34
help.go
|
@ -29,8 +29,9 @@ help topics
|
|||
commands: Basics of x-1 commands, and a full listing of them. Each
|
||||
command also has its own help topic.
|
||||
urls: The forms of URLs which can be entered into x-1 commands.
|
||||
mark: Information on the "mark" meta-command.
|
||||
tour: Information about the "tour" meta-command.
|
||||
mark: Information on the bookmarks and the "mark" meta-command.
|
||||
tour: Information about tours and the "tour" meta-command.
|
||||
identity: Identities and managing them with the "identity" meta-command.
|
||||
config: The x-1 configuration file.
|
||||
`[1:],
|
||||
|
||||
|
@ -42,7 +43,7 @@ back forward
|
|||
next previous
|
||||
reload print pipe
|
||||
help links history
|
||||
tour mark
|
||||
tour mark identity
|
||||
go save
|
||||
about quit
|
||||
|
||||
|
@ -114,7 +115,8 @@ look them up again. Marks are preserved across x-1 sessions.
|
|||
|
||||
The mark meta-command has multiple sub-commands which can be used to
|
||||
manage and navigate to your saved marks. "m[ark] X" with any mark name
|
||||
or unique prefix of a name can be used as "mark go".
|
||||
or unique prefix of a name can be used as "mark go", and "m[ark]" alone
|
||||
is treated as "mark list".
|
||||
|
||||
m[ark] a[dd] NAME URL: adds a new name/url pair to your saved marks.
|
||||
m[ark] g[o] NAME: navigates to the named mark's URL.
|
||||
|
@ -145,6 +147,30 @@ t[our] s[elect] [NAME]: make the named tour active (optionally named
|
|||
by a unique prefix), or without a name, selects the default tour.
|
||||
`[1:],
|
||||
|
||||
"identity": `
|
||||
i[dentity]
|
||||
----------
|
||||
An identity is a managed credential in the form of a TLS client
|
||||
certificate. This meta-command supports managing your various identities
|
||||
and assigning them to be used on particular domains or on specific
|
||||
pages.
|
||||
|
||||
i[dentity] c[reate] NAME: create a new identity (TLS key/certificate).
|
||||
i[dentity] l[ist]: list identities and the domains and paths on which
|
||||
they are assigned to be used.
|
||||
i[dentity] u[se] NAME d[omain] DOMAIN: assign the named identity to be
|
||||
used across a given domain.
|
||||
i[dentity] u[se] NAME f[older] URL: assign an identity to be used on any
|
||||
path which has URL as a prefix.
|
||||
i[dentity] u[se] NAME p[age] URL: always use the named identity on a
|
||||
specific page.
|
||||
i[dentity] d[elete] NAME: remove the named identity and any
|
||||
domain/folder/page associations it has.
|
||||
|
||||
Any "identity use" command will override existing associations to the
|
||||
same domain/folder/page.
|
||||
`[1:],
|
||||
|
||||
"root": `
|
||||
r[oot]
|
||||
------
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Identities struct {
|
||||
ByName map[string]*tls.Config
|
||||
ByDomain map[string]*tls.Config
|
||||
ByPage map[string]*tls.Config
|
||||
ByFolder map[string]*tls.Config
|
||||
}
|
||||
|
||||
func findIdentity(state *BrowserState, prefix string) (string, error) {
|
||||
found := 0
|
||||
value := ""
|
||||
for name := range state.Identities.ByName {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
found += 1
|
||||
value = name
|
||||
}
|
||||
}
|
||||
|
||||
switch found {
|
||||
case 0:
|
||||
return "", errors.New("no matching identity found")
|
||||
case 1:
|
||||
return value, nil
|
||||
default:
|
||||
return "", fmt.Errorf("too ambiguous - that name matched %d identities", found)
|
||||
}
|
||||
}
|
||||
|
||||
func (ids Identities) Get(u *url.URL) *tls.Config {
|
||||
if conf, ok := ids.ByPage[u.String()]; ok {
|
||||
return conf
|
||||
}
|
||||
|
||||
pathsegments := strings.Split(strings.TrimLeft(u.Path, "/"), "/")
|
||||
for len(pathsegments) > 0 {
|
||||
pathsegments = pathsegments[0 : len(pathsegments)-1]
|
||||
if conf, ok := ids.ByFolder[u.Hostname()+"/"+strings.Join(pathsegments, "/")]; ok {
|
||||
return conf
|
||||
}
|
||||
}
|
||||
|
||||
if conf, ok := ids.ByDomain[u.Hostname()]; ok {
|
||||
return conf
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IdentityCreate(state *BrowserState, name string) error {
|
||||
ident, err := createIdentity(state, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state.Identities.ByName[name] = ident
|
||||
return saveIdentities(state.Identities)
|
||||
}
|
||||
|
||||
func IdentityList(state *BrowserState) error {
|
||||
buf := &bytes.Buffer{}
|
||||
for name, ident := range state.Identities.ByName {
|
||||
if _, err := fmt.Fprintf(buf, "%s:\n", name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for domain, id := range state.Identities.ByDomain {
|
||||
if id == ident {
|
||||
if _, err := fmt.Fprintf(buf, " domain %s\n", domain); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for folder, id := range state.Identities.ByFolder {
|
||||
if id == ident {
|
||||
if _, err := fmt.Fprintf(buf, " folder %s\n", folder); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for page, id := range state.Identities.ByPage {
|
||||
if id == ident {
|
||||
if _, err := fmt.Fprintf(buf, " page %s\n", page); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err := io.Copy(os.Stdout, buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func IdentityDelete(state *BrowserState, name string) error {
|
||||
name, err := findIdentity(state, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ident := state.Identities.ByName[name]
|
||||
delete(state.Identities.ByName, name)
|
||||
|
||||
for domain, id := range state.Identities.ByDomain {
|
||||
if id == ident {
|
||||
delete(state.Identities.ByDomain, domain)
|
||||
}
|
||||
}
|
||||
for folder, id := range state.Identities.ByFolder {
|
||||
if id == ident {
|
||||
delete(state.Identities.ByFolder, folder)
|
||||
}
|
||||
}
|
||||
for page, id := range state.Identities.ByPage {
|
||||
if id == ident {
|
||||
delete(state.Identities.ByPage, page)
|
||||
}
|
||||
}
|
||||
|
||||
if err := removeIdentity(name); err != nil {
|
||||
return err
|
||||
}
|
||||
return saveIdentities(state.Identities)
|
||||
}
|
||||
|
||||
func IdentityUseDomain(state *BrowserState, name string, domain string) error {
|
||||
name, err := findIdentity(state, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ident := state.Identities.ByName[name]
|
||||
|
||||
u, _, err := parseURL(domain, state, "gemini")
|
||||
if errors.Is(err, ErrInvalidLink) {
|
||||
u, err = url.Parse(domain)
|
||||
if err != nil {
|
||||
return ErrInvalidLink
|
||||
}
|
||||
if u.Hostname() == "" {
|
||||
u.Host = domain
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state.Identities.ByDomain[u.Hostname()] = ident
|
||||
return saveIdentities(state.Identities)
|
||||
}
|
||||
|
||||
func IdentityUseFolder(state *BrowserState, name string, domain string) error {
|
||||
name, err := findIdentity(state, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ident := state.Identities.ByName[name]
|
||||
|
||||
u, _, err := parseURL(domain, state, "gemini")
|
||||
if errors.Is(err, ErrInvalidLink) {
|
||||
u, err = url.Parse(domain)
|
||||
if err != nil {
|
||||
return ErrInvalidLink
|
||||
}
|
||||
if u.Hostname() == "" {
|
||||
u.Host = domain
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state.Identities.ByFolder[fmt.Sprintf("%s/%s", u.Hostname(), u.Path)] = ident
|
||||
return saveIdentities(state.Identities)
|
||||
}
|
||||
|
||||
func IdentityUsePage(state *BrowserState, name string, domain string) error {
|
||||
name, err := findIdentity(state, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ident := state.Identities.ByName[name]
|
||||
|
||||
u, _, err := parseURL(domain, state, "gemini")
|
||||
if errors.Is(err, ErrInvalidLink) {
|
||||
u, err = url.Parse(domain)
|
||||
if err != nil {
|
||||
return ErrInvalidLink
|
||||
}
|
||||
if u.Hostname() == "" {
|
||||
u.Host = domain
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state.Identities.ByPage[u.String()] = ident
|
||||
return saveIdentities(state.Identities)
|
||||
}
|
13
main.go
13
main.go
|
@ -8,7 +8,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"tildegit.org/tjp/sliderule"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -21,11 +20,7 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
client = sliderule.NewClient(tlsConfig())
|
||||
|
||||
state := NewBrowserState()
|
||||
state.Quiet = conf.Quiet
|
||||
state.Pager = conf.Pager
|
||||
state := NewBrowserState(conf)
|
||||
|
||||
rl, err := readline.New(Prompt)
|
||||
if err != nil {
|
||||
|
@ -44,6 +39,12 @@ func main() {
|
|||
}
|
||||
state.NamedTours = tours
|
||||
|
||||
idents, err := getIdentities()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
state.Identities = idents
|
||||
|
||||
if conf.VimKeys {
|
||||
rl.SetVimMode(true)
|
||||
}
|
||||
|
|
7
state.go
7
state.go
|
@ -13,6 +13,8 @@ type BrowserState struct {
|
|||
|
||||
Marks map[string]string
|
||||
|
||||
Identities Identities
|
||||
|
||||
NamedTours map[string]*Tour
|
||||
DefaultTour Tour
|
||||
CurrentTour *Tour
|
||||
|
@ -48,13 +50,16 @@ type Link struct {
|
|||
Prompt bool
|
||||
}
|
||||
|
||||
func NewBrowserState() *BrowserState {
|
||||
func NewBrowserState(conf *Config) *BrowserState {
|
||||
state := &BrowserState{
|
||||
History: &History{
|
||||
Url: nil,
|
||||
Depth: 0,
|
||||
NavIndex: -1,
|
||||
},
|
||||
|
||||
Quiet: conf.Quiet,
|
||||
Pager: conf.Pager,
|
||||
}
|
||||
state.CurrentTour = &state.DefaultTour
|
||||
return state
|
||||
|
|
90
tls.go
90
tls.go
|
@ -1,24 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"math/big"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func tlsConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
VerifyConnection: tofuVerify,
|
||||
func tlsConfig(state *BrowserState) *tls.Config {
|
||||
if ident := state.Identities.Get(state.Url); ident != nil {
|
||||
return ident
|
||||
}
|
||||
return anonymousTLS
|
||||
}
|
||||
|
||||
var tofuStore map[string]string
|
||||
|
||||
var ErrTOFUViolation = errors.New("certificate for this domain has changed")
|
||||
|
||||
var anonymousTLS = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
VerifyConnection: tofuVerify,
|
||||
}
|
||||
|
||||
func tofuVerify(connState tls.ConnectionState) error {
|
||||
certhash, err := hashCert(connState.PeerCertificates[0])
|
||||
if err != nil {
|
||||
|
@ -45,3 +56,74 @@ func hashCert(cert *x509.Certificate) (string, error) {
|
|||
hash := sha256.Sum256(pubkeybytes)
|
||||
return hex.EncodeToString(hash[:]), nil
|
||||
}
|
||||
|
||||
func createIdentity(state *BrowserState, name string) (*tls.Config, error) {
|
||||
pubkey, privkey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawprivkey, err := x509.MarshalPKCS8PrivateKey(privkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commonName := name
|
||||
state.Readline.SetPrompt("Common Name [" + name + "]: ")
|
||||
if line, err := state.Readline.Readline(); err != nil {
|
||||
return nil, err
|
||||
} else if line != "" {
|
||||
commonName = line
|
||||
}
|
||||
|
||||
expiration := time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC)
|
||||
state.Readline.SetPrompt("Expiration (yyyy-mm-dd) [9999-12-31]: ")
|
||||
if line, err := state.Readline.Readline(); err != nil {
|
||||
return nil, err
|
||||
} else if line != "" {
|
||||
expiration, err = time.ParseInLocation(time.DateOnly, line, time.UTC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
snLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, snLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{CommonName: commonName},
|
||||
NotAfter: expiration,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
rawcert, err := x509.CreateCertificate(rand.Reader, template, template, pubkey, privkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identFile, err := saveIdentity(name, rawprivkey, rawcert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(identFile, identFile)
|
||||
if err != nil {
|
||||
_ = os.Remove(identFile)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identityForCert(cert), nil
|
||||
}
|
||||
|
||||
func identityForCert(cert tls.Certificate) *tls.Config {
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
InsecureSkipVerify: true,
|
||||
VerifyConnection: tofuVerify,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue