// Starts the AMQP-RPC server running in a separate thread. // There is currently no Stop() method. func (rpc *AmqpRPCServer) Start() (err error) { msgs, err := amqpSubscribe(rpc.channel, rpc.serverQueue, rpc.log) if err != nil { return } go func() { for msg := range msgs { // XXX-JWS: jws.Verify(body) cb, present := rpc.dispatchTable[msg.Type] rpc.log.Info(fmt.Sprintf(" [s<] received %s(%s) [%s]", msg.Type, core.B64enc(msg.Body), msg.CorrelationId)) if !present { // AUDIT[ Misrouted Messages ] f523f21f-12d2-4c31-b2eb-ee4b7d96d60e rpc.log.Audit(fmt.Sprintf("Misrouted message: %s - %s - %s", msg.Type, core.B64enc(msg.Body), msg.CorrelationId)) continue } response := cb(msg.Body) rpc.log.Info(fmt.Sprintf(" [s>] sending %s(%s) [%s]", msg.Type, core.B64enc(response), msg.CorrelationId)) rpc.channel.Publish( AmqpExchange, msg.ReplyTo, AmqpMandatory, AmqpImmediate, amqp.Publishing{ CorrelationId: msg.CorrelationId, Type: msg.Type, Body: response, // XXX-JWS: jws.Sign(privKey, body) }) } }() return }
func TestDvsni(t *testing.T) { va := NewValidationAuthorityImpl(true) va.DNSResolver = core.NewDNSResolver(time.Second*5, []string{"8.8.8.8:53"}) a := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} ba := core.B64enc(a) chall := core.Challenge{R: ba, S: ba} invalidChall, err := va.validateDvsni(ident, chall) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "Server's not up yet; expected refusal. Where did we connect?") test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem) waitChan := make(chan bool, 1) stopChan := make(chan bool, 1) go dvsniSrv(t, a, a, stopChan, waitChan) defer func() { stopChan <- true }() <-waitChan finChall, err := va.validateDvsni(ident, chall) test.AssertEquals(t, finChall.Status, core.StatusValid) test.AssertNotError(t, err, "") invalidChall, err = va.validateDvsni(core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}, chall) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "IdentifierType IP shouldn't have worked.") test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem) va.TestMode = false invalidChall, err = va.validateDvsni(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "Domain name is invalid.") test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem) va.TestMode = true chall.R = ba[5:] invalidChall, err = va.validateDvsni(ident, chall) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "R Should be illegal Base64") test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem) chall.R = ba chall.S = "!@#" invalidChall, err = va.validateDvsni(ident, chall) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "S Should be illegal Base64") test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem) chall.S = ba chall.Nonce = "wait-long" started := time.Now() invalidChall, err = va.validateDvsni(ident, chall) took := time.Since(started) // Check that the HTTP connection times out after 5 seconds and doesn't block for 10 seconds test.Assert(t, (took > (time.Second * 5)), "HTTP timed out before 5 seconds") test.Assert(t, (took < (time.Second * 10)), "HTTP connection didn't timeout after 5 seconds") test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "Connection should've timed out") test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem) }
func NewAmqpRPCCLient(clientQueue, serverQueue string, channel *amqp.Channel) (rpc *AmqpRPCCLient, err error) { rpc = &AmqpRPCCLient{ serverQueue: serverQueue, clientQueue: clientQueue, channel: channel, pending: make(map[string]chan []byte), timeout: 10 * time.Second, log: blog.GetAuditLogger(), } // Subscribe to the response queue and dispatch msgs, err := amqpSubscribe(rpc.channel, clientQueue, nil) if err != nil { return } go func() { for msg := range msgs { // XXX-JWS: jws.Sign(privKey, body) corrID := msg.CorrelationId responseChan, present := rpc.pending[corrID] rpc.log.Debug(fmt.Sprintf(" [c<] received %s(%s) [%s]", msg.Type, core.B64enc(msg.Body), corrID)) if !present { continue } responseChan <- msg.Body delete(rpc.pending, corrID) } }() return }
func dvsniSrv(t *testing.T, chall core.Challenge, stopChan, waitChan chan bool) { encodedSig := core.B64enc(chall.Validation.Signatures[0].Signature) h := sha256.New() h.Write([]byte(encodedSig)) Z := hex.EncodeToString(h.Sum(nil)) ZName := fmt.Sprintf("%s.%s.acme.invalid", Z[:32], Z[32:]) template := &x509.Certificate{ SerialNumber: big.NewInt(1337), Subject: pkix.Name{ Organization: []string{"tests"}, }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 1), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, DNSNames: []string{ZName}, } certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey) cert := &tls.Certificate{ Certificate: [][]byte{certBytes}, PrivateKey: &TheKey, } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{*cert}, ClientAuth: tls.NoClientCert, GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { if clientHello.ServerName != ZName { time.Sleep(time.Second * 10) return nil, nil } return cert, nil }, NextProtos: []string{"http/1.1"}, } httpsServer := &http.Server{Addr: "localhost:5001"} conn, err := net.Listen("tcp", httpsServer.Addr) if err != nil { waitChan <- true t.Fatalf("Couldn't listen on %s: %s", httpsServer.Addr, err) } tlsListener := tls.NewListener(conn, tlsConfig) go func() { <-stopChan conn.Close() }() waitChan <- true httpsServer.Serve(tlsListener) }
func (va ValidationAuthorityImpl) validateDNS(identifier core.AcmeIdentifier, input core.Challenge, accountKey jose.JsonWebKey) (core.Challenge, error) { challenge := input if identifier.Type != core.IdentifierDNS { challenge.Error = &core.ProblemDetails{ Type: core.MalformedProblem, Detail: "Identifier type for DNS was not itself DNS", } va.log.Debug(fmt.Sprintf("DNS [%s] Identifier failure", identifier)) challenge.Status = core.StatusInvalid return challenge, challenge.Error } // Check that JWS body is as expected // * "type" == "dvsni" // * "token" == challenge.token target := map[string]interface{}{ "type": core.ChallengeTypeDNS, "token": challenge.Token, } err := verifyValidationJWS((*jose.JsonWebSignature)(challenge.Validation), &accountKey, target) if err != nil { va.log.Debug(err.Error()) challenge.Status = core.StatusInvalid challenge.Error = &core.ProblemDetails{ Type: core.UnauthorizedProblem, Detail: err.Error(), } return challenge, err } encodedSignature := core.B64enc(challenge.Validation.Signatures[0].Signature) // Look for the required record in the DNS challengeSubdomain := fmt.Sprintf("%s.%s", core.DNSPrefix, identifier.Value) txts, _, err := va.DNSResolver.LookupTXT(challengeSubdomain) if err != nil { challenge.Status = core.StatusInvalid setChallengeErrorFromDNSError(err, &challenge) va.log.Debug(fmt.Sprintf("%s [%s] DNS failure: %s", challenge.Type, identifier, err)) return challenge, challenge.Error } for _, element := range txts { if subtle.ConstantTimeCompare([]byte(element), []byte(encodedSignature)) == 1 { challenge.Status = core.StatusValid return challenge, nil } } challenge.Error = &core.ProblemDetails{ Type: core.UnauthorizedProblem, Detail: "Correct value not found for DNS challenge", } challenge.Status = core.StatusInvalid return challenge, challenge.Error }
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method func dvsniSrv(t *testing.T, chall core.Challenge) *httptest.Server { encodedSig := core.B64enc(chall.Validation.Signatures[0].Signature) h := sha256.New() h.Write([]byte(encodedSig)) Z := hex.EncodeToString(h.Sum(nil)) ZName := fmt.Sprintf("%s.%s.acme.invalid", Z[:32], Z[32:]) template := &x509.Certificate{ SerialNumber: big.NewInt(1337), Subject: pkix.Name{ Organization: []string{"tests"}, }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 1), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, DNSNames: []string{ZName}, } certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey) cert := &tls.Certificate{ Certificate: [][]byte{certBytes}, PrivateKey: &TheKey, } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{*cert}, ClientAuth: tls.NoClientCert, GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { if clientHello.ServerName != ZName { time.Sleep(time.Second * 10) return nil, nil } return cert, nil }, NextProtos: []string{"http/1.1"}, } hs := httptest.NewUnstartedServer(http.DefaultServeMux) hs.TLS = tlsConfig hs.StartTLS() return hs }
func TestTLSError(t *testing.T) { va := NewValidationAuthorityImpl(true) va.DNSResolver = core.NewDNSResolver(time.Second*5, []string{"8.8.8.8:53"}) a := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} ba := core.B64enc(a) chall := core.Challenge{R: ba, S: ba} waitChan := make(chan bool, 1) stopChan := make(chan bool, 1) go brokenTLSSrv(t, stopChan, waitChan) defer func() { stopChan <- true }() <-waitChan invalidChall, err := va.validateDvsni(ident, chall) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "What cert was used?") test.AssertEquals(t, invalidChall.Error.Type, core.TLSProblem) }
func (rpc *AmqpRPCCLient) Dispatch(method string, body []byte) chan []byte { // Create a channel on which to direct the response // At least in some cases, it's important that this channel // be buffered to avoid deadlock responseChan := make(chan []byte, 1) corrID := core.NewToken() rpc.pending[corrID] = responseChan // Send the request rpc.log.Debug(fmt.Sprintf(" [c>] sending %s(%s) [%s]", method, core.B64enc(body), corrID)) rpc.channel.Publish( AmqpExchange, rpc.serverQueue, AmqpMandatory, AmqpImmediate, amqp.Publishing{ CorrelationId: corrID, ReplyTo: rpc.clientQueue, Type: method, Body: body, // XXX-JWS: jws.Sign(privKey, body) }) return responseChan }
func TestDvsni(t *testing.T) { va := NewValidationAuthorityImpl(true) a := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} ba := core.B64enc(a) chall := core.Challenge{R: ba, S: ba} invalidChall, err := va.validateDvsni(ident, chall) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "Server's not up yet; expected refusal. Where did we connect?") waitChan := make(chan bool, 1) stopChan := make(chan bool, 1) go dvsniSrv(t, a, a, stopChan, waitChan) defer func() { stopChan <- true }() <-waitChan finChall, err := va.validateDvsni(ident, chall) test.AssertEquals(t, finChall.Status, core.StatusValid) test.AssertNotError(t, err, "") chall.R = ba[5:] invalidChall, err = va.validateDvsni(ident, chall) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "R Should be illegal Base64") invalidChall, err = va.validateSimpleHTTPS(core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}, chall) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "Forgot path; that should be an error.") chall.R = ba chall.S = "!@#" invalidChall, err = va.validateDvsni(ident, chall) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertError(t, err, "S Should be illegal Base64") }
// NewAmqpRPCClient constructs an RPC client using AMQP func NewAmqpRPCClient(clientQueuePrefix, serverQueue string, channel *amqp.Channel) (rpc *AmqpRPCCLient, err error) { hostname, err := os.Hostname() if err != nil { return nil, err } clientQueue := fmt.Sprintf("%s.%s", clientQueuePrefix, hostname) rpc = &AmqpRPCCLient{ serverQueue: serverQueue, clientQueue: clientQueue, channel: channel, pending: make(map[string]chan []byte), timeout: 10 * time.Second, log: blog.GetAuditLogger(), } // Subscribe to the response queue and dispatch msgs, err := amqpSubscribe(rpc.channel, clientQueue, "", rpc.log) if err != nil { return nil, err } go func() { for msg := range msgs { // XXX-JWS: jws.Sign(privKey, body) corrID := msg.CorrelationId rpc.mu.Lock() responseChan, present := rpc.pending[corrID] rpc.mu.Unlock() rpc.log.Debug(fmt.Sprintf(" [c<][%s] response %s(%s) [%s]", clientQueue, msg.Type, core.B64enc(msg.Body), corrID)) if !present { // AUDIT[ Misrouted Messages ] f523f21f-12d2-4c31-b2eb-ee4b7d96d60e rpc.log.Audit(fmt.Sprintf(" [c<][%s] Misrouted message: %s - %s - %s", clientQueue, msg.Type, core.B64enc(msg.Body), msg.CorrelationId)) continue } responseChan <- msg.Body rpc.mu.Lock() delete(rpc.pending, corrID) rpc.mu.Unlock() } }() return rpc, err }
func (rpc *AmqpRPCServer) processMessage(msg amqp.Delivery) { // XXX-JWS: jws.Verify(body) cb, present := rpc.dispatchTable[msg.Type] rpc.log.Info(fmt.Sprintf(" [s<][%s][%s] received %s(%s) [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, core.B64enc(msg.Body), msg.CorrelationId)) if !present { // AUDIT[ Misrouted Messages ] f523f21f-12d2-4c31-b2eb-ee4b7d96d60e rpc.log.Audit(fmt.Sprintf(" [s<][%s][%s] Misrouted message: %s - %s - %s", rpc.serverQueue, msg.ReplyTo, msg.Type, core.B64enc(msg.Body), msg.CorrelationId)) return } var response RPCResponse var err error response.ReturnVal, err = cb(msg.Body) response.Error = wrapError(err) jsonResponse, err := json.Marshal(response) if err != nil { // AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3 rpc.log.Audit(fmt.Sprintf(" [s>][%s][%s] Error condition marshalling RPC response %s [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, msg.CorrelationId)) return } if response.Error.Value != "" { rpc.log.Info(fmt.Sprintf(" [s>][%s][%s] %s failed, replying: %s (%s) [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, response.Error.Value, response.Error.Type, msg.CorrelationId)) } rpc.log.Debug(fmt.Sprintf(" [s>][%s][%s] replying %s(%s) [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, core.B64enc(jsonResponse), msg.CorrelationId)) rpc.Channel.Publish( AmqpExchange, msg.ReplyTo, AmqpMandatory, AmqpImmediate, amqp.Publishing{ CorrelationId: msg.CorrelationId, Type: msg.Type, Body: jsonResponse, // XXX-JWS: jws.Sign(privKey, body) }) }
func (va ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier, input core.Challenge, accountKey jose.JsonWebKey) (core.Challenge, error) { challenge := input if identifier.Type != "dns" { challenge.Error = &core.ProblemDetails{ Type: core.MalformedProblem, Detail: "Identifier type for DVSNI was not DNS", } challenge.Status = core.StatusInvalid va.log.Debug(fmt.Sprintf("DVSNI [%s] Identifier failure", identifier)) return challenge, challenge.Error } // Check that JWS body is as expected // * "type" == "dvsni" // * "token" == challenge.token target := map[string]interface{}{ "type": core.ChallengeTypeDVSNI, "token": challenge.Token, } err := verifyValidationJWS((*jose.JsonWebSignature)(challenge.Validation), &accountKey, target) if err != nil { va.log.Debug(err.Error()) challenge.Status = core.StatusInvalid challenge.Error = &core.ProblemDetails{ Type: core.UnauthorizedProblem, Detail: err.Error(), } return challenge, err } // Compute the digest that will appear in the certificate encodedSignature := core.B64enc(challenge.Validation.Signatures[0].Signature) h := sha256.New() h.Write([]byte(encodedSignature)) Z := hex.EncodeToString(h.Sum(nil)) ZName := fmt.Sprintf("%s.%s.%s", Z[:32], Z[32:], core.DVSNISuffix) // Make a connection with SNI = nonceName hostPort := identifier.Value + ":443" if va.TestMode { hostPort = "localhost:5001" } va.log.Notice(fmt.Sprintf("DVSNI [%s] Attempting to validate DVSNI for %s %s", identifier, hostPort, ZName)) conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", hostPort, &tls.Config{ ServerName: ZName, InsecureSkipVerify: true, }) if err != nil { challenge.Status = core.StatusInvalid challenge.Error = &core.ProblemDetails{ Type: parseHTTPConnError(err), Detail: "Failed to connect to host for DVSNI challenge", } va.log.Debug(fmt.Sprintf("DVSNI [%s] TLS Connection failure: %s", identifier, err)) return challenge, err } defer conn.Close() // Check that ZName is a dNSName SAN in the server's certificate certs := conn.ConnectionState().PeerCertificates if len(certs) == 0 { challenge.Error = &core.ProblemDetails{ Type: core.UnauthorizedProblem, Detail: "No certs presented for DVSNI challenge", } challenge.Status = core.StatusInvalid return challenge, challenge.Error } for _, name := range certs[0].DNSNames { if subtle.ConstantTimeCompare([]byte(name), []byte(ZName)) == 1 { challenge.Status = core.StatusValid return challenge, nil } } challenge.Error = &core.ProblemDetails{ Type: core.UnauthorizedProblem, Detail: "Correct ZName not found for DVSNI challenge", } challenge.Status = core.StatusInvalid return challenge, challenge.Error }
// dispatch sends a body to the destination, and returns the id for the request // that can be used to correlate it with responses, and a response channel that // can be used to monitor for responses, or discarded for one-shot actions. func (rpc *AmqpRPCCLient) dispatch(method string, body []byte) (string, chan []byte) { // Create a channel on which to direct the response // At least in some cases, it's important that this channel // be buffered to avoid deadlock responseChan := make(chan []byte, 1) corrID := core.NewToken() rpc.mu.Lock() rpc.pending[corrID] = responseChan rpc.mu.Unlock() // Send the request rpc.log.Debug(fmt.Sprintf(" [c>][%s] requesting %s(%s) [%s]", rpc.clientQueue, method, core.B64enc(body), corrID)) rpc.connection.publish( rpc.serverQueue, corrID, "30000", rpc.clientQueue, method, body) return corrID, responseChan }
// NewAmqpRPCClient constructs an RPC client using AMQP func NewAmqpRPCClient(clientQueuePrefix, serverQueue string, c cmd.Config, stats statsd.Statter) (rpc *AmqpRPCCLient, err error) { hostname, err := os.Hostname() if err != nil { return nil, err } randID := make([]byte, 3) _, err = rand.Read(randID) if err != nil { return nil, err } clientQueue := fmt.Sprintf("%s.%s.%x", clientQueuePrefix, hostname, randID) reconnectBase := c.AMQP.ReconnectTimeouts.Base.Duration if reconnectBase == 0 { reconnectBase = 20 * time.Millisecond } reconnectMax := c.AMQP.ReconnectTimeouts.Max.Duration if reconnectMax == 0 { reconnectMax = time.Minute } rpc = &AmqpRPCCLient{ serverQueue: serverQueue, clientQueue: clientQueue, connection: newAMQPConnector(clientQueue, reconnectBase, reconnectMax), pending: make(map[string]chan []byte), timeout: 10 * time.Second, log: blog.GetAuditLogger(), stats: stats, } err = rpc.connection.connect(c) if err != nil { return nil, err } go func() { for { select { case msg, ok := <-rpc.connection.messages(): if ok { corrID := msg.CorrelationId rpc.mu.RLock() responseChan, present := rpc.pending[corrID] rpc.mu.RUnlock() if !present { // occurs when a request is timed out and the arrives // afterwards stats.Inc("RPC.AfterTimeoutResponseArrivals."+clientQueuePrefix, 1, 1.0) continue } rpc.log.Debug(fmt.Sprintf(" [c<][%s] response %s(%s) [%s]", clientQueue, msg.Type, core.B64enc(msg.Body), corrID)) responseChan <- msg.Body rpc.mu.Lock() delete(rpc.pending, corrID) rpc.mu.Unlock() } else { // chan has been closed by rpc.connection.Cancel rpc.log.Info(fmt.Sprintf(" [!] Client reply channel closed: %s", rpc.clientQueue)) continue } case err = <-rpc.connection.closeChannel(): rpc.log.Info(fmt.Sprintf(" [!] Client reply channel closed : %s", rpc.clientQueue)) rpc.connection.reconnect(c, rpc.log) } } }() return rpc, err }
// Start starts the AMQP-RPC server running in a separate thread. // There is currently no Stop() method. func (rpc *AmqpRPCServer) Start() (err error) { msgs, err := amqpSubscribe(rpc.channel, rpc.serverQueue, rpc.log) if err != nil { return } go func() { for msg := range msgs { // XXX-JWS: jws.Verify(body) cb, present := rpc.dispatchTable[msg.Type] rpc.log.Info(fmt.Sprintf(" [s<][%s][%s] received %s(%s) [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, core.B64enc(msg.Body), msg.CorrelationId)) if !present { // AUDIT[ Misrouted Messages ] f523f21f-12d2-4c31-b2eb-ee4b7d96d60e rpc.log.Audit(fmt.Sprintf(" [s<][%s][%s] Misrouted message: %s - %s - %s", rpc.serverQueue, msg.ReplyTo, msg.Type, core.B64enc(msg.Body), msg.CorrelationId)) continue } var response RPCResponse response.ReturnVal, err = cb(msg.Body) response.Error = wrapError(err) jsonResponse, err := json.Marshal(response) if err != nil { // AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3 rpc.log.Audit(fmt.Sprintf(" [s>][%s][%s] Error condition marshalling RPC response %s [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, msg.CorrelationId)) continue } rpc.log.Info(fmt.Sprintf(" [s>][%s][%s] replying %s(%s) [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, core.B64enc(jsonResponse), msg.CorrelationId)) rpc.channel.Publish( AmqpExchange, msg.ReplyTo, AmqpMandatory, AmqpImmediate, amqp.Publishing{ CorrelationId: msg.CorrelationId, Type: msg.Type, Body: jsonResponse, // XXX-JWS: jws.Sign(privKey, body) }) } }() return }