// makeCookie creates the cookie to be sent with initial meek HTTP request. // The purpose of the cookie is to send the following to the server: // ServerAddress -- the Psiphon Server address the meek server should relay to // SessionID -- the Psiphon session ID (used by meek server to relay geolocation // information obtained from the CDN through to the Psiphon Server) // MeekProtocolVersion -- tells the meek server that this client understands // the latest protocol. // The server will create a session using these values and send the session ID // back to the client via Set-Cookie header. Client must use that value with // all consequent HTTP requests // In unfronted meek mode, the cookie is visible over the adversary network, so the // cookie is encrypted and obfuscated. func makeMeekCookie(meekConfig *MeekConfig) (cookie *http.Cookie, err error) { // Make the JSON data serverAddress := meekConfig.PsiphonServerAddress cookieData := &meekCookieData{ ServerAddress: serverAddress, SessionID: meekConfig.SessionID, MeekProtocolVersion: MEEK_PROTOCOL_VERSION, } serializedCookie, err := json.Marshal(cookieData) if err != nil { return nil, common.ContextError(err) } // Encrypt the JSON data // NaCl box is used for encryption. The peer public key comes from the server entry. // Nonce is always all zeros, and is not sent in the cookie (the server also uses an all-zero nonce). // http://nacl.cace-project.eu/box.html: // "There is no harm in having the same nonce for different messages if the {sender, receiver} sets are // different. This is true even if the sets overlap. For example, a sender can use the same nonce for two // different messages if the messages are sent to two different public keys." var nonce [24]byte var publicKey [32]byte decodedPublicKey, err := base64.StdEncoding.DecodeString(meekConfig.MeekCookieEncryptionPublicKey) if err != nil { return nil, common.ContextError(err) } copy(publicKey[:], decodedPublicKey) ephemeralPublicKey, ephemeralPrivateKey, err := box.GenerateKey(rand.Reader) if err != nil { return nil, common.ContextError(err) } box := box.Seal(nil, serializedCookie, &nonce, &publicKey, ephemeralPrivateKey) encryptedCookie := make([]byte, 32+len(box)) copy(encryptedCookie[0:32], ephemeralPublicKey[0:32]) copy(encryptedCookie[32:], box) // Obfuscate the encrypted data obfuscator, err := common.NewClientObfuscator( &common.ObfuscatorConfig{Keyword: meekConfig.MeekObfuscatedKey, MaxPadding: MEEK_COOKIE_MAX_PADDING}) if err != nil { return nil, common.ContextError(err) } obfuscatedCookie := obfuscator.SendSeedMessage() seedLen := len(obfuscatedCookie) obfuscatedCookie = append(obfuscatedCookie, encryptedCookie...) obfuscator.ObfuscateClientToServer(obfuscatedCookie[seedLen:]) // Format the HTTP cookie // The format is <random letter 'A'-'Z'>=<base64 data>, which is intended to match common cookie formats. A := int('A') Z := int('Z') // letterIndex is integer in range [int('A'), int('Z')] letterIndex, err := common.MakeSecureRandomInt(Z - A + 1) if err != nil { return nil, common.ContextError(err) } return &http.Cookie{ Name: string(byte(A + letterIndex)), Value: base64.StdEncoding.EncodeToString(obfuscatedCookie)}, nil }
// GenerateConfig creates a new Psiphon server config. It returns JSON // encoded configs and a client-compatible "server entry" for the server. It // generates all necessary secrets and key material, which are emitted in // the config file and server entry as necessary. // GenerateConfig uses sample values for many fields. The intention is for // generated configs to be used for testing or as a template for production // setup, not to generate production-ready configurations. func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error) { // Input validation if net.ParseIP(params.ServerIPAddress) == nil { return nil, nil, nil, common.ContextError(errors.New("invalid IP address")) } if len(params.TunnelProtocolPorts) == 0 { return nil, nil, nil, common.ContextError(errors.New("no tunnel protocols")) } usedPort := make(map[int]bool) if params.WebServerPort != 0 { usedPort[params.WebServerPort] = true } usingMeek := false for tunnelProtocol, port := range params.TunnelProtocolPorts { if !common.Contains(protocol.SupportedTunnelProtocols, tunnelProtocol) { return nil, nil, nil, common.ContextError(errors.New("invalid tunnel protocol")) } if usedPort[port] { return nil, nil, nil, common.ContextError(errors.New("duplicate listening port")) } usedPort[port] = true if protocol.TunnelProtocolUsesMeekHTTP(tunnelProtocol) || protocol.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) { usingMeek = true } } // Web server config var webServerSecret, webServerCertificate, webServerPrivateKey, webServerPortForwardAddress string if params.WebServerPort != 0 { var err error webServerSecret, err = common.MakeRandomStringHex(WEB_SERVER_SECRET_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.ContextError(err) } webServerCertificate, webServerPrivateKey, err = GenerateWebServerCertificate("") if err != nil { return nil, nil, nil, common.ContextError(err) } webServerPortForwardAddress = net.JoinHostPort( params.ServerIPAddress, strconv.Itoa(params.WebServerPort)) } // SSH config // TODO: use other key types: anti-fingerprint by varying params rsaKey, err := rsa.GenerateKey(rand.Reader, SSH_RSA_HOST_KEY_BITS) if err != nil { return nil, nil, nil, common.ContextError(err) } sshPrivateKey := pem.EncodeToMemory( &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaKey), }, ) signer, err := ssh.NewSignerFromKey(rsaKey) if err != nil { return nil, nil, nil, common.ContextError(err) } sshPublicKey := signer.PublicKey() sshUserNameSuffix, err := common.MakeRandomStringHex(SSH_USERNAME_SUFFIX_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.ContextError(err) } sshUserName := "******" + sshUserNameSuffix sshPassword, err := common.MakeRandomStringHex(SSH_PASSWORD_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.ContextError(err) } // TODO: vary version string for anti-fingerprint sshServerVersion := "SSH-2.0-Psiphon" // Obfuscated SSH config obfuscatedSSHKey, err := common.MakeRandomStringHex(SSH_OBFUSCATED_KEY_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.ContextError(err) } // Meek config var meekCookieEncryptionPublicKey, meekCookieEncryptionPrivateKey, meekObfuscatedKey string if usingMeek { rawMeekCookieEncryptionPublicKey, rawMeekCookieEncryptionPrivateKey, err := box.GenerateKey(rand.Reader) if err != nil { return nil, nil, nil, common.ContextError(err) } meekCookieEncryptionPublicKey = base64.StdEncoding.EncodeToString(rawMeekCookieEncryptionPublicKey[:]) meekCookieEncryptionPrivateKey = base64.StdEncoding.EncodeToString(rawMeekCookieEncryptionPrivateKey[:]) meekObfuscatedKey, err = common.MakeRandomStringHex(SSH_OBFUSCATED_KEY_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.ContextError(err) } } // Other config discoveryValueHMACKey, err := common.MakeRandomStringBase64(DISCOVERY_VALUE_KEY_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.ContextError(err) } // Assemble configs and server entry // Note: this config is intended for either testing or as an illustrative // example or template and is not intended for production deployment. config := &Config{ LogLevel: "info", LogFilename: params.LogFilename, GeoIPDatabaseFilenames: nil, HostID: "example-host-id", ServerIPAddress: params.ServerIPAddress, DiscoveryValueHMACKey: discoveryValueHMACKey, WebServerPort: params.WebServerPort, WebServerSecret: webServerSecret, WebServerCertificate: webServerCertificate, WebServerPrivateKey: webServerPrivateKey, WebServerPortForwardAddress: webServerPortForwardAddress, SSHPrivateKey: string(sshPrivateKey), SSHServerVersion: sshServerVersion, SSHUserName: sshUserName, SSHPassword: sshPassword, ObfuscatedSSHKey: obfuscatedSSHKey, TunnelProtocolPorts: params.TunnelProtocolPorts, DNSResolverIPAddress: "8.8.8.8", UDPInterceptUdpgwServerAddress: "127.0.0.1:7300", MeekCookieEncryptionPrivateKey: meekCookieEncryptionPrivateKey, MeekObfuscatedKey: meekObfuscatedKey, MeekCertificateCommonName: "www.example.org", MeekProhibitedHeaders: nil, MeekProxyForwardedForHeaders: []string{"X-Forwarded-For"}, LoadMonitorPeriodSeconds: 300, TrafficRulesFilename: params.TrafficRulesFilename, } encodedConfig, err := json.MarshalIndent(config, "\n", " ") if err != nil { return nil, nil, nil, common.ContextError(err) } intPtr := func(i int) *int { return &i } trafficRulesSet := &TrafficRulesSet{ DefaultRules: TrafficRules{ RateLimits: RateLimits{ ReadUnthrottledBytes: new(int64), ReadBytesPerSecond: new(int64), WriteUnthrottledBytes: new(int64), WriteBytesPerSecond: new(int64), }, IdleTCPPortForwardTimeoutMilliseconds: intPtr(DEFAULT_IDLE_TCP_PORT_FORWARD_TIMEOUT_MILLISECONDS), IdleUDPPortForwardTimeoutMilliseconds: intPtr(DEFAULT_IDLE_UDP_PORT_FORWARD_TIMEOUT_MILLISECONDS), MaxTCPPortForwardCount: intPtr(DEFAULT_MAX_TCP_PORT_FORWARD_COUNT), MaxUDPPortForwardCount: intPtr(DEFAULT_MAX_UDP_PORT_FORWARD_COUNT), AllowTCPPorts: nil, AllowUDPPorts: nil, }, } encodedTrafficRulesSet, err := json.MarshalIndent(trafficRulesSet, "\n", " ") if err != nil { return nil, nil, nil, common.ContextError(err) } capabilities := []string{} if params.EnableSSHAPIRequests { capabilities = append(capabilities, protocol.CAPABILITY_SSH_API_REQUESTS) } if params.WebServerPort != 0 { capabilities = append(capabilities, protocol.CAPABILITY_UNTUNNELED_WEB_API_REQUESTS) } for tunnelProtocol, _ := range params.TunnelProtocolPorts { capabilities = append(capabilities, protocol.GetCapability(tunnelProtocol)) } sshPort := params.TunnelProtocolPorts["SSH"] obfuscatedSSHPort := params.TunnelProtocolPorts["OSSH"] // Meek port limitations // - fronted meek protocols are hard-wired in the client to be port 443 or 80. // - only one other meek port may be specified. meekPort := params.TunnelProtocolPorts["UNFRONTED-MEEK-OSSH"] if meekPort == 0 { meekPort = params.TunnelProtocolPorts["UNFRONTED-MEEK-HTTPS-OSSH"] } // Note: fronting params are a stub; this server entry will exercise // client and server fronting code paths, but not actually traverse // a fronting hop. serverEntryWebServerPort := "" strippedWebServerCertificate := "" if params.WebServerPort != 0 { serverEntryWebServerPort = fmt.Sprintf("%d", params.WebServerPort) // Server entry format omits the BEGIN/END lines and newlines lines := strings.Split(webServerCertificate, "\n") strippedWebServerCertificate = strings.Join(lines[1:len(lines)-2], "") } serverEntry := &protocol.ServerEntry{ IpAddress: params.ServerIPAddress, WebServerPort: serverEntryWebServerPort, WebServerSecret: webServerSecret, WebServerCertificate: strippedWebServerCertificate, SshPort: sshPort, SshUsername: sshUserName, SshPassword: sshPassword, SshHostKey: base64.RawStdEncoding.EncodeToString(sshPublicKey.Marshal()), SshObfuscatedPort: obfuscatedSSHPort, SshObfuscatedKey: obfuscatedSSHKey, Capabilities: capabilities, Region: "US", MeekServerPort: meekPort, MeekCookieEncryptionPublicKey: meekCookieEncryptionPublicKey, MeekObfuscatedKey: meekObfuscatedKey, MeekFrontingHosts: []string{params.ServerIPAddress}, MeekFrontingAddresses: []string{params.ServerIPAddress}, MeekFrontingDisableSNI: false, } encodedServerEntry, err := protocol.EncodeServerEntry(serverEntry) if err != nil { return nil, nil, nil, common.ContextError(err) } return encodedConfig, encodedTrafficRulesSet, []byte(encodedServerEntry), nil }