vestauth 0.7.0 β 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/README.md +91 -21
- package/package.json +1 -1
- package/src/cli/actions/agent/curl.js +35 -28
- package/src/lib/helpers/authorityMessage.js +7 -0
- package/src/lib/helpers/errors.js +11 -0
- package/src/lib/helpers/headers.js +1 -1
- package/src/lib/helpers/parseSignatureAgentHeader.js +3 -2
- package/src/lib/helpers/providerVerify.js +11 -4
- package/src/lib/helpers/verify.js +2 -2
- package/src/lib/helpers/verifyAgentFqdn.js +28 -0
- package/src/lib/helpers/webBotAuthSignature.js +0 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
-
[Unreleased](https://github.com/vestauth/vestauth/compare/v0.7.
|
|
5
|
+
[Unreleased](https://github.com/vestauth/vestauth/compare/v0.7.2...main)
|
|
6
|
+
|
|
7
|
+
## [0.7.2](https://github.com/vestauth/vestauth/compare/v0.7.1...v0.7.2) (2026-02-04)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
* Handle headers in a more flexible way
|
|
12
|
+
|
|
13
|
+
## [0.7.1](https://github.com/vestauth/vestauth/compare/v0.7.0...v0.7.1) (2026-02-04)
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
* Raise an error if a bad `Signature-Agent` header ([#11](https://github.com/vestauth/vestauth/pull/11))
|
|
6
18
|
|
|
7
19
|
## [0.7.0](https://github.com/vestauth/vestauth/compare/v0.6.0...v0.7.0) (2026-02-03)
|
|
8
20
|
|
package/README.md
CHANGED
|
@@ -2,67 +2,137 @@
|
|
|
2
2
|
|
|
3
3
|
*auth for agents*βfrom the creator of [`dotenvx`](https://github.com/dotenvx/dotenvx).
|
|
4
4
|
|
|
5
|
-
* identity
|
|
5
|
+
* identity (cryptographic)
|
|
6
6
|
* authentication
|
|
7
|
+
* verification
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
### Quickstart [](https://www.npmjs.com/package/vestauth) [](https://www.npmjs.com/package/vestauth)
|
|
11
12
|
|
|
12
13
|
```sh
|
|
13
|
-
|
|
14
|
+
npm i -g vestauth
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
```sh
|
|
17
18
|
vestauth agent init
|
|
18
|
-
vestauth agent curl https://api.vestauth.com/whoami
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
or install
|
|
23
|
+
or install globally - *unlocks vestauth for any agent, agent tool, or agent framework!*
|
|
24
24
|
|
|
25
|
-
<details><summary>with
|
|
25
|
+
<details><summary>with curl π </summary><br>
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
|
|
28
|
+
curl -sfS https://vestauth.sh | sh
|
|
29
|
+
vestauth help
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
[](https://github.com/vestauth/vestauth.sh/blob/main/install.sh)
|
|
33
|
+
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
</details>
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
<details><summary>with github releases π</summary><br>
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
curl -L -o vestauth.tar.gz "https://github.com/vestauth/vestauth/releases/latest/download/vestauth-$(uname -s)-$(uname -m).tar.gz"
|
|
42
|
+
tar -xzf vestauth.tar.gz
|
|
43
|
+
./vestauth help
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
[](https://github.com/vestauth/vestauth/releases)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
</details>
|
|
36
51
|
|
|
37
|
-
>
|
|
52
|
+
<details><summary>or windows πͺ</summary><br>
|
|
53
|
+
|
|
54
|
+
Download [the windows executable](https://github.com/vestauth/vestauth/releases) directly from the [releases page](https://github.com/vestauth/vestauth/releases).
|
|
55
|
+
|
|
56
|
+
> * [vestauth-windows-amd64.zip
|
|
57
|
+
](https://github.com/vestauth/releases/raw/main/latest/vestauth-windows-amd64.zip)
|
|
58
|
+
> * [vestauth-windows-x86_64.zip
|
|
59
|
+
](https://github.com/vestauth/releases/raw/main/latest/vestauth-windows-x86_64.zip)
|
|
60
|
+
|
|
61
|
+
(unzip to extract `vestauth.exe`)
|
|
62
|
+
|
|
63
|
+
</details>
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Identity
|
|
68
|
+
|
|
69
|
+
> Give your agent its cryptographic identity.
|
|
38
70
|
|
|
39
71
|
```sh
|
|
72
|
+
$ mkdir your-agent
|
|
73
|
+
$ cd your-agent
|
|
74
|
+
|
|
40
75
|
$ vestauth agent init
|
|
41
|
-
|
|
76
|
+
β agent created (.env/AGENT_ID=agent-4b94ccd425e939fac5016b6b)
|
|
42
77
|
```
|
|
43
78
|
|
|
44
|
-
|
|
79
|
+
<details><summary>learn more</summary><br>
|
|
45
80
|
|
|
46
|
-
|
|
81
|
+
This populates a `.env` file with an `AGENT_PUBLIC_JWK`, `AGENT_PRIVATE_JWK`, and `AGENT_ID`.
|
|
47
82
|
|
|
48
|
-
|
|
83
|
+
```ini
|
|
84
|
+
# example
|
|
85
|
+
AGENT_PUBLIC_JWK="{"crv":"Ed25519","x":"py2xNaAfjKZiau-jtmJls6h_3n8xJ1Ur0ie-n9b8zWg","kty":"OKP","kid":"B0u80Gw28W9U2Jl5t_EBiWeBajO2104kOYZ9Ikucl5I"}"
|
|
86
|
+
AGENT_PRIVATE_JWK="{"crv":"Ed25519","d":"Z9vbwN-3eiFMVv_TPWXOxqSMJAT21kZvejWi72yiAaQ","x":"py2xNaAfjKZiau-jtmJls6h_3n8xJ1Ur0ie-n9b8zWg","kty":"OKP","kid":"B0u80Gw28W9U2Jl5t_EBiWeBajO2104kOYZ9Ikucl5I"}"
|
|
87
|
+
AGENT_ID="agent-4b94ccd425e939fac5016b6b"
|
|
88
|
+
```
|
|
49
89
|
|
|
50
|
-
|
|
90
|
+
* The `AGENT_PUBLIC_KEY` is auto-hosted to its own [`/.well-known/http-message-signatures-directory`](https://datatracker.ietf.org/doc/html/draft-meunier-http-message-signatures-directory-04#appendix-A) for discovery purposes.
|
|
91
|
+
* The `AGENT_PRIVATE_KEY` must NOT be shared and is used to sign requests according to [RFC 9421](https://datatracker.ietf.org/doc/rfc9421/).
|
|
92
|
+
* The `AGENT_ID` contributes to building the [FQDN for the `Signature-Agent` header](https://datatracker.ietf.org/doc/html/draft-meunier-http-message-signatures-directory-01#name-request-with-http-signature).
|
|
93
|
+
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## Authentication
|
|
99
|
+
|
|
100
|
+
> Turn any curl request into a signed, authenticated request.
|
|
51
101
|
|
|
52
102
|
```sh
|
|
53
|
-
|
|
103
|
+
> without vestauth
|
|
104
|
+
$ curl https://api.vestauth.com/whoami
|
|
105
|
+
{"error":{"status":400,"code":null,"message":"bad_request","help":null,"meta":null}}
|
|
106
|
+
|
|
107
|
+
> with vestauth
|
|
108
|
+
$ vestauth agent curl https://api.vestauth.com/whoami
|
|
109
|
+
{"uid":"agent-4b94ccd425e939fac5016b6b",...}
|
|
54
110
|
```
|
|
55
111
|
|
|
56
|
-
|
|
112
|
+
<details><summary>learn more</summary><br>
|
|
57
113
|
|
|
58
|
-
|
|
59
|
-
* Next.js
|
|
60
|
-
* Rails
|
|
61
|
-
* ...
|
|
114
|
+
Vestauth autosigns each curl request β injecting valid signed headers according to the [web-bot-auth draft](https://datatracker.ietf.org/doc/html/draft-meunier-web-bot-auth-architecture). View these with the built-in `headers` primitive.
|
|
62
115
|
|
|
63
|
-
|
|
116
|
+
```
|
|
117
|
+
$ vestauth primitives headers GET https://api.vestauth.com/whoami --pp
|
|
118
|
+
{
|
|
119
|
+
"Signature": "sig1=:d4Id5SXhUExsf1XyruD8eBmlDtWzt/vezoCS+SKf0M8CxSkhKBtdHH7KkYyMN6E0hmxmNHsYus11u32nhvpWBQ==:",
|
|
120
|
+
"Signature-Input": "sig1=(\"@authority\");created=1770247189;keyid=\"B0u80Gw28W9U2Jl5t_EBiWeBajO2104kOYZ9Ikucl5I\";alg=\"ed25519\";expires=1770247489;nonce=\"NURxn28X7zyKJ9k5bHxuOyO5qdvF9L5s2qHmhTrGUzbwGSIoUCHmwSlwiiCRgTDGuum83yyWMHJU4jmrVI_XPg\";tag=\"web-bot-auth\"",
|
|
121
|
+
"Signature-Agent": "sig1=agent-4b94ccd425e939fac5016b6b.agents.vestauth.com"
|
|
122
|
+
}
|
|
123
|
+
```
|
|
64
124
|
|
|
65
|
-
|
|
125
|
+
</details>
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
## Verification
|
|
130
|
+
|
|
131
|
+
> As a provider of agentic tools, authenticate agents through cryptographic verification.
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
|
|
135
|
+
```
|
|
66
136
|
|
|
67
137
|
## Advanced
|
|
68
138
|
|
package/package.json
CHANGED
|
@@ -2,37 +2,44 @@ const { logger } = require('./../../../shared/logger')
|
|
|
2
2
|
const agent = require('./../../../lib/agent')
|
|
3
3
|
const execute = require('./../../../lib/helpers/execute')
|
|
4
4
|
const findUrl = require('./../../../lib/helpers/findUrl')
|
|
5
|
+
const catchAndLog = require('./../../../lib/helpers/catchAndLog')
|
|
5
6
|
|
|
6
7
|
async function curl () {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
8
|
+
try {
|
|
9
|
+
const commandArgs = this.args
|
|
10
|
+
logger.debug(`process command [${commandArgs.join(' ')}]`)
|
|
11
|
+
|
|
12
|
+
const options = this.opts()
|
|
13
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
14
|
+
|
|
15
|
+
const httpMethod = 'GET'
|
|
16
|
+
const url = findUrl(commandArgs)
|
|
17
|
+
const headers = await agent.headers(httpMethod, url)
|
|
18
|
+
const injected = [
|
|
19
|
+
'curl',
|
|
20
|
+
'-H', `Signature: ${headers.Signature}`,
|
|
21
|
+
'-H', `Signature-Input: ${headers['Signature-Input']}`,
|
|
22
|
+
'-H', `Signature-Agent: ${headers['Signature-Agent']}`,
|
|
23
|
+
...commandArgs
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const { stdout, exitCode } = await execute.execa(injected[0], injected.slice(1), {})
|
|
27
|
+
|
|
28
|
+
if (exitCode !== 0) {
|
|
29
|
+
logger.debug(`received exitCode ${exitCode}`)
|
|
30
|
+
throw new Error(`Command exited with exit code ${exitCode}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let space = 0
|
|
34
|
+
if (options.prettyPrint) {
|
|
35
|
+
space = 2
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(JSON.stringify(JSON.parse(stdout), null, space))
|
|
39
|
+
} catch (error) {
|
|
40
|
+
catchAndLog(error)
|
|
41
|
+
process.exit(1)
|
|
28
42
|
}
|
|
29
|
-
|
|
30
|
-
let space = 0
|
|
31
|
-
if (options.prettyPrint) {
|
|
32
|
-
space = 2
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
console.log(JSON.stringify(JSON.parse(stdout), null, space))
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
module.exports = curl
|
|
@@ -2,6 +2,13 @@ function authorityMessage (uri, signatureParams) {
|
|
|
2
2
|
const u = new URL(uri)
|
|
3
3
|
const authority = u.host // includes port if present
|
|
4
4
|
|
|
5
|
+
// other possible message (target-uri, and signature-params)
|
|
6
|
+
// const message = [
|
|
7
|
+
// `"@method": ${method.toUpperCase()}`,
|
|
8
|
+
// `"@target-uri": ${uri}`,
|
|
9
|
+
// `"@signature-params": ${signatureParams}`
|
|
10
|
+
// ].join('\n')
|
|
11
|
+
|
|
5
12
|
return [
|
|
6
13
|
`"@authority": ${authority}`,
|
|
7
14
|
`"@signature-params": ${signatureParams}`
|
|
@@ -36,6 +36,17 @@ class Errors {
|
|
|
36
36
|
return e
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
invalidSignatureAgent () {
|
|
40
|
+
const code = 'INVALID_SIGNATURE_AGENT'
|
|
41
|
+
const message = `[${code}] invalid --signature-agent`
|
|
42
|
+
const help = `[${code}] https://github.com/vestauth/vestauth/issues/11`
|
|
43
|
+
|
|
44
|
+
const e = new Error(message)
|
|
45
|
+
e.code = code
|
|
46
|
+
e.help = help
|
|
47
|
+
return e
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
econnrefused () {
|
|
40
51
|
const code = 'ECONNREFUSED'
|
|
41
52
|
const message = `[${code}] connection refused`
|
|
@@ -33,7 +33,7 @@ async function headers (httpMethod, uri, id, privateJwk, tag = 'web-bot-auth', n
|
|
|
33
33
|
|
|
34
34
|
const signatureInput = signatureParams(privateJwk.kid, tag, nonce)
|
|
35
35
|
const signature = webBotAuthSignature(httpMethod, uri, signatureInput, privateJwk)
|
|
36
|
-
const signatureAgent =
|
|
36
|
+
const signatureAgent = `${id}.agents.vestauth.com` // agent-1234.agents.vestauth.com (no scheme) /.well-known/http-message-signatures-directory
|
|
37
37
|
|
|
38
38
|
return {
|
|
39
39
|
Signature: `sig1=:${signature}:`,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
const { parseDictionary } = require('structured-headers')
|
|
2
|
+
const Errors = require('./errors')
|
|
2
3
|
|
|
3
4
|
// example: sig1=https://agent-9aa52a556ca85ee195866c0b.agents.vestauth.com
|
|
4
5
|
function parseSignatureAgentHeader (signatureAgentHeader) {
|
|
5
6
|
const dictionary = parseDictionary(signatureAgentHeader)
|
|
6
7
|
const entry = dictionary.entries().next()
|
|
7
|
-
if (entry.done) throw new
|
|
8
|
+
if (entry.done) throw new Errors().invalidSignatureAgent()
|
|
8
9
|
const [key, innerlist] = entry.value
|
|
9
10
|
const value = innerlist[0].value
|
|
10
|
-
if (!value) throw new
|
|
11
|
+
if (!value) throw new Errors().invalidSignatureAgent()
|
|
11
12
|
|
|
12
13
|
return {
|
|
13
14
|
key,
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
const { http } = require('./http')
|
|
2
2
|
const buildApiError = require('./buildApiError')
|
|
3
3
|
const parseSignatureAgentHeader = require('./parseSignatureAgentHeader')
|
|
4
|
+
const verifyAgentFqdn = require('./verifyAgentFqdn')
|
|
4
5
|
const verify = require('./verify')
|
|
5
6
|
|
|
6
|
-
async function providerVerify (httpMethod
|
|
7
|
-
const signatureAgent = headers['Signature-Agent']
|
|
8
|
-
const { value } = parseSignatureAgentHeader(signatureAgent) // sig1=
|
|
7
|
+
async function providerVerify (httpMethod, uri, headers = {}) {
|
|
8
|
+
const signatureAgent = headers['Signature-Agent'] || headers['signature-agent'] // support either case (expressjs lowers headers)
|
|
9
|
+
const { value } = parseSignatureAgentHeader(signatureAgent) // sig1=agent-1234.agents.vestauth.com
|
|
10
|
+
const fqdn = value
|
|
11
|
+
verifyAgentFqdn(fqdn)
|
|
12
|
+
const origin = `https://${fqdn}` // https://agent-1234.agents.vestauth.com
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
// get jwks from .well-known
|
|
15
|
+
const url = `${origin}/.well-known/http-message-signatures-directory` // risk of SSRF ?
|
|
11
16
|
const resp = await http(url, { method: 'GET', headers: { 'Content-Type': 'application/json' } })
|
|
12
17
|
if (resp.statusCode >= 400) {
|
|
13
18
|
const json = await resp.body.json()
|
|
@@ -26,6 +31,8 @@ async function providerVerify (httpMethod = 'GET', uri = 'https://api.vestauth.c
|
|
|
26
31
|
// }
|
|
27
32
|
// ]
|
|
28
33
|
// }
|
|
34
|
+
|
|
35
|
+
// use publicJwk to verify
|
|
29
36
|
const publicJwk = json.keys[0]
|
|
30
37
|
const output = await verify(httpMethod, uri, headers, publicJwk)
|
|
31
38
|
|
|
@@ -6,8 +6,8 @@ const authorityMessage = require('./authorityMessage')
|
|
|
6
6
|
const publicJwkObject = require('./publicJwkObject')
|
|
7
7
|
|
|
8
8
|
function verify (httpMethod, uri, headers = {}, publicJwk) {
|
|
9
|
-
const signature = headers.Signature
|
|
10
|
-
const signatureInput = headers['Signature-Input']
|
|
9
|
+
const signature = headers.Signature || headers.signature
|
|
10
|
+
const signatureInput = headers['Signature-Input'] || headers['signature-input']
|
|
11
11
|
|
|
12
12
|
const { values } = parseSignatureInputHeader(signatureInput)
|
|
13
13
|
const { expires } = values
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const DEFAULT_PROVIDER_FQDN_REGEX = /^[A-Za-z0-9-]+\.agents\.vestauth\.com$/
|
|
2
|
+
const Errors = require('./errors')
|
|
3
|
+
|
|
4
|
+
function getProviderFqdnRegex () {
|
|
5
|
+
const override = process.env.PROVIDER_FQDN_REGEX
|
|
6
|
+
if (!override) return DEFAULT_PROVIDER_FQDN_REGEX
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
return new RegExp(override)
|
|
10
|
+
} catch {
|
|
11
|
+
return DEFAULT_PROVIDER_FQDN_REGEX
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function verifyAgentFqdn (fqdn) {
|
|
16
|
+
if (!fqdn || typeof fqdn !== 'string') {
|
|
17
|
+
throw new Errors().invalidSignatureAgent()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const pattern = getProviderFqdnRegex()
|
|
21
|
+
if (!pattern.test(fqdn)) {
|
|
22
|
+
throw new Errors().invalidSignatureAgent()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = verifyAgentFqdn
|
|
@@ -5,12 +5,6 @@ const authorityMessage = require('./authorityMessage')
|
|
|
5
5
|
function webBotAuthSignature (method = 'GET', uri = '', signatureParams, privateJwk) {
|
|
6
6
|
const message = authorityMessage(uri, signatureParams)
|
|
7
7
|
|
|
8
|
-
// const message = [
|
|
9
|
-
// `"@method": ${method.toUpperCase()}`,
|
|
10
|
-
// `"@target-uri": ${uri}`,
|
|
11
|
-
// `"@signature-params": ${signatureParams}`
|
|
12
|
-
// ].join('\n')
|
|
13
|
-
|
|
14
8
|
return crypto.sign(
|
|
15
9
|
null,
|
|
16
10
|
Buffer.from(message, 'utf8'),
|