feat(agent): verify native ARI identity before real execution
This commit is contained in:
@@ -35,6 +35,10 @@ The test host's address and raw logs are omitted from committed evidence.
|
||||
- After read-only confirmation of **zero active calls and channels**, the committed and locally tested `configure-asterisk-user-ari.sh` (`1d12007`) created only the `rogee` user's private `ari.conf`, `http.conf` and `ari-secret`, all mode `0600`; no existing file was overwritten and no credential was printed or committed. HTTP binds only `127.0.0.1:8088`. The user-level Asterisk service was then explicitly restarted and reported **enabled+active** with a live CLI; anonymous ARI status returned **401**, and a read-only authenticated ARI information request returned **200**. No outbound call, registration or media capture was performed.
|
||||
- Very short independent SSH probes had observed the service active before its control socket appeared. In a sustained SSH session, its PID stayed stable and the socket and CLI became available after startup; this was a session/startup observation, not evidence of a broken Asterisk binary. Lingering is **still disabled**: availability after logout or reboot remains unverified. Real Agent ARI use, signed task/provider snapshots, carrier requirements and capture-first host validation have **not** been accepted.
|
||||
|
||||
## Native Agent ARI SDK readback — 2026-10-04
|
||||
|
||||
- On the same pinned test host, an explicitly opt-in, CGO-disabled Go test read the owner-only `ari-secret` **in memory** (no value logged) and used the existing `github.com/CyCoreSystems/ari/v5/client/native` client against the loopback ARI endpoint. `native.Connect` and the authenticated read-only Asterisk status request both passed. The test binary was temporary and removed after the check; no channel was created, no call originated, no RTP/audio was captured and no OSS or AI request was issued. `internal/asterisk/ari.go` fails closed for missing, non-regular, non-`0600`, malformed or mismatched credentials. SDK readback does **not** establish real Agent execution, Stasis/media handling, recording or carrier acceptance.
|
||||
|
||||
## Still required before a real call or production acceptance
|
||||
|
||||
1. Verify the effect of endpoint updates **during an active authorized call** separately; SIP-only does not enable business dialing. Unsupported authentication/REGISTER and transport changes must continue to fail closed, and provider-side authentication, registration, routing and capacity remain unverified.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package asterisk
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/CyCoreSystems/ari/v5"
|
||||
"github.com/CyCoreSystems/ari/v5/client/native"
|
||||
)
|
||||
|
||||
// ariOptions uses the credential issued to the Agent on the same Cell as
|
||||
// native Asterisk. It never reads a historical local test credential.
|
||||
func (l Loader) ariOptions() (*native.Options, error) {
|
||||
if l.ConfigDir == "" {
|
||||
return nil, errors.New("native Asterisk configuration directory required")
|
||||
}
|
||||
path := filepath.Join(l.ConfigDir, "ari-secret")
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect issued Agent ARI credential: %T", err)
|
||||
}
|
||||
if !info.Mode().IsRegular() || info.Mode().Perm() != 0600 {
|
||||
return nil, errors.New("issued Agent ARI credential must be a private regular file (0600)")
|
||||
}
|
||||
secret, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read issued Agent ARI credential: %T", err)
|
||||
}
|
||||
if len(secret) != 64 {
|
||||
return nil, errors.New("issued Agent ARI credential has an invalid length")
|
||||
}
|
||||
if _, err := hex.DecodeString(string(secret)); err != nil {
|
||||
return nil, errors.New("issued Agent ARI credential has an invalid format")
|
||||
}
|
||||
return &native.Options{
|
||||
URL: "http://127.0.0.1:8088/ari", WebsocketURL: "ws://127.0.0.1:8088/ari/events",
|
||||
Application: "go-sip-agent", Username: "go-sip-agent", Password: string(secret),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OpenARI proves that the native server accepts this Agent credential without
|
||||
// creating a channel or dialing. The caller owns and must close the client.
|
||||
func (l Loader) OpenARI() (ari.Client, error) {
|
||||
options, err := l.ariOptions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := native.Connect(options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect native Agent ARI: %T", err)
|
||||
}
|
||||
info, err := client.Asterisk().Info(nil)
|
||||
if err != nil {
|
||||
client.Close()
|
||||
return nil, fmt.Errorf("read native Agent ARI status: %T", err)
|
||||
}
|
||||
if info == nil {
|
||||
client.Close()
|
||||
return nil, errors.New("native Agent ARI returned an empty status")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package asterisk
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Explicitly opt in on the Cell to check real ARI credentials without
|
||||
// creating a channel, issuing a dial or reading private audio.
|
||||
func TestAgentARIReadOnlyIntegration(t *testing.T) {
|
||||
configDir := os.Getenv("AGENT_ARI_READONLY_TEST_CONFIG_DIR")
|
||||
if configDir == "" {
|
||||
t.Skip("real Agent ARI readback requires explicit Cell config path")
|
||||
}
|
||||
client, err := (Loader{ConfigDir: configDir}).OpenARI()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package asterisk
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAgentARIOptionsRequirePrivateIssuedCredential(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "ari-secret")
|
||||
secret := strings.Repeat("a", 64)
|
||||
if err := os.WriteFile(path, []byte(secret), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
options, err := (Loader{ConfigDir: dir}).ariOptions()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if options.URL != "http://127.0.0.1:8088/ari" || options.WebsocketURL != "ws://127.0.0.1:8088/ari/events" || options.Application != "go-sip-agent" || options.Username != "go-sip-agent" || options.Password != secret {
|
||||
t.Fatal("ARI must bind the issued user identity and loopback endpoint")
|
||||
}
|
||||
if err := os.Chmod(path, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := (Loader{ConfigDir: dir}).ariOptions(); err == nil || strings.Contains(err.Error(), secret) {
|
||||
t.Fatalf("public credential must fail without leaking its value: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user