File
Blob: tun/server/acme_rpc.go
| 1 | package server |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "crypto/ed25519" |
| 7 | "errors" |
| 8 | "strings" |
| 9 | "time" |
| 10 | |
| 11 | "go.miragespace.co/specter/spec/acme" |
| 12 | "go.miragespace.co/specter/spec/chord" |
| 13 | "go.miragespace.co/specter/spec/pow" |
| 14 | "go.miragespace.co/specter/spec/protocol" |
| 15 | "go.miragespace.co/specter/spec/tun" |
| 16 | |
| 17 | "github.com/twitchtv/twirp" |
| 18 | "go.uber.org/zap" |
| 19 | ) |
| 20 | |
| 21 | func (s *Server) checkAcme(ctx context.Context, hostname string, proof *protocol.ProofOfWork, token *protocol.ClientToken, client *protocol.Node) (found bool, err error) { |
| 22 | _, err = pow.VerifySolution(proof, pow.Parameters{ |
| 23 | Difficulty: acme.HashcashDifficulty, |
| 24 | Expires: acme.HashcashExpires, |
| 25 | GetSubject: func(pubKey ed25519.PublicKey) string { |
| 26 | return hostname |
| 27 | }, |
| 28 | }) |
| 29 | if err != nil { |
| 30 | return false, err |
| 31 | } |
| 32 | |
| 33 | if strings.Contains(hostname, s.Acme) || strings.Contains(hostname, s.Apex) { |
| 34 | return false, twirp.InvalidArgumentError("hostname", "provided hostname is not valid for custom hostname") |
| 35 | } |
| 36 | |
| 37 | if strings.Count(hostname, ".") < 2 { |
| 38 | return false, twirp.InvalidArgumentError("hostname", "custom domain is not supported on bare domain") |
| 39 | } |
| 40 | |
| 41 | bundle, err := tun.FindCustomHostname(ctx, s.Chord, hostname) |
| 42 | switch err { |
| 43 | case nil: |
| 44 | // alreday exist |
| 45 | if !bytes.Equal(bundle.GetClientToken().GetToken(), token.GetToken()) || |
| 46 | bundle.GetClientIdentity().GetId() != client.GetId() || |
| 47 | bundle.GetClientIdentity().GetAddress() != client.GetAddress() { |
| 48 | return false, twirp.InvalidArgumentError("hostname", "provided hostname is not valid for custom hostname") |
| 49 | } |
| 50 | return true, nil |
| 51 | case tun.ErrHostnameNotFound: |
| 52 | // expected |
| 53 | return false, nil |
| 54 | default: |
| 55 | return false, err |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | func (s *Server) AcmeInstruction(ctx context.Context, req *protocol.InstructionRequest) (*protocol.InstructionResponse, error) { |
| 60 | token, client, err := extractAuthenticated(ctx) |
| 61 | if err != nil { |
| 62 | return nil, err |
| 63 | } |
| 64 | |
| 65 | hostname, err := acme.Normalize(req.GetHostname()) |
| 66 | if err != nil { |
| 67 | return nil, twirp.InvalidArgumentError("hostname", err.Error()) |
| 68 | } |
| 69 | |
| 70 | if _, err := s.checkAcme(ctx, hostname, req.GetProof(), token, client); err != nil { |
| 71 | return nil, err |
| 72 | } |
| 73 | |
| 74 | name, content := acme.GenerateCustomRecord(hostname, s.Acme, token.GetToken()) |
| 75 | return &protocol.InstructionResponse{ |
| 76 | Name: name, |
| 77 | Content: content, |
| 78 | }, nil |
| 79 | } |
| 80 | |
| 81 | func (s *Server) AcmeValidate(ctx context.Context, req *protocol.ValidateRequest) (*protocol.ValidateResponse, error) { |
| 82 | token, client, err := extractAuthenticated(ctx) |
| 83 | if err != nil { |
| 84 | return nil, err |
| 85 | } |
| 86 | |
| 87 | hostname, err := acme.Normalize(req.GetHostname()) |
| 88 | if err != nil { |
| 89 | return nil, twirp.InvalidArgumentError("hostname", err.Error()) |
| 90 | } |
| 91 | |
| 92 | found, err := s.checkAcme(ctx, hostname, req.GetProof(), token, client) |
| 93 | if err != nil { |
| 94 | return nil, err |
| 95 | } |
| 96 | |
| 97 | var ( |
| 98 | start time.Time |
| 99 | cname string |
| 100 | ) |
| 101 | |
| 102 | name, content := acme.GenerateCustomRecord(hostname, s.Acme, token.GetToken()) |
| 103 | |
| 104 | lookupCtx, lookupCancel := context.WithTimeout(ctx, lookupTimeout) |
| 105 | defer lookupCancel() |
| 106 | |
| 107 | if found { |
| 108 | goto VALIDATED |
| 109 | } |
| 110 | |
| 111 | start = time.Now() |
| 112 | cname, err = s.Resolver.LookupCNAME(lookupCtx, name) |
| 113 | if err != nil { |
| 114 | return nil, twirp.FailedPrecondition.Error(err.Error()) |
| 115 | } |
| 116 | |
| 117 | if cname != content { |
| 118 | return nil, twirp.FailedPrecondition.Errorf("unexpected CNAME content: %s", cname) |
| 119 | } |
| 120 | |
| 121 | s.Logger.Info("Custom hostname validated", zap.String("hostname", hostname), zap.Duration("took", time.Since(start)), zap.Object("client", client)) |
| 122 | |
| 123 | VALIDATED: |
| 124 | if err := tun.SaveCustomHostname(ctx, s.Chord, hostname, &protocol.CustomHostname{ |
| 125 | ClientIdentity: client, |
| 126 | ClientToken: token, |
| 127 | }); err != nil { |
| 128 | return nil, twirp.InternalErrorWith(err) |
| 129 | } |
| 130 | |
| 131 | prefix := tun.ClientHostnamesPrefix(token) |
| 132 | err = s.Chord.PrefixAppend(ctx, []byte(prefix), []byte(hostname)) |
| 133 | if err != nil && !errors.Is(err, chord.ErrKVPrefixConflict) { |
| 134 | return nil, twirp.InternalErrorWith(err) |
| 135 | } |
| 136 | |
| 137 | return &protocol.ValidateResponse{ |
| 138 | Apex: s.Apex, |
| 139 | }, nil |
| 140 | } |