valleydam 0.1.0__tar.gz
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.
- valleydam-0.1.0/PKG-INFO +185 -0
- valleydam-0.1.0/README.md +158 -0
- valleydam-0.1.0/setup.cfg +4 -0
- valleydam-0.1.0/setup.py +37 -0
- valleydam-0.1.0/src/valleydam/__init__.py +16 -0
- valleydam-0.1.0/src/valleydam/cli.py +61 -0
- valleydam-0.1.0/src/valleydam/client.py +37 -0
- valleydam-0.1.0/src/valleydam/core.py +102 -0
- valleydam-0.1.0/src/valleydam/verifier.py +87 -0
- valleydam-0.1.0/src/valleydam.egg-info/PKG-INFO +185 -0
- valleydam-0.1.0/src/valleydam.egg-info/SOURCES.txt +13 -0
- valleydam-0.1.0/src/valleydam.egg-info/dependency_links.txt +1 -0
- valleydam-0.1.0/src/valleydam.egg-info/entry_points.txt +2 -0
- valleydam-0.1.0/src/valleydam.egg-info/requires.txt +3 -0
- valleydam-0.1.0/src/valleydam.egg-info/top_level.txt +1 -0
valleydam-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: valleydam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A DNS-based cryptographic identity verification protocol for AI Agents.
|
|
5
|
+
Home-page: https://github.com/supra-nlpn/valley-dam
|
|
6
|
+
Author: Supra N.
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/supra-nlpn/valley-dam/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests>=2.25.0
|
|
16
|
+
Requires-Dist: cryptography>=3.4.0
|
|
17
|
+
Requires-Dist: dnspython>=2.1.0
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: project-url
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
# ⛰️ ValleyDam
|
|
29
|
+
|
|
30
|
+
ValleyDam is a lightweight, open protocol for verifying the identity of AI agents and web scrapers using **DNS-backed cryptographic proof**.
|
|
31
|
+
|
|
32
|
+
It enables a website to verify that a request *actually* came from `bot.openai.com` (or your startup’s domain) **without** API keys, IP allowlists, or complex authentication handshakes.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## The Problem
|
|
37
|
+
|
|
38
|
+
Today, websites have no reliable way to identify automated clients.
|
|
39
|
+
|
|
40
|
+
- **User-Agent strings are lies**
|
|
41
|
+
Anyone can send `User-Agent: Googlebot`.
|
|
42
|
+
|
|
43
|
+
- **IP blocking is messy**
|
|
44
|
+
Legitimate bots often run on shared cloud infrastructure (AWS, GCP).
|
|
45
|
+
|
|
46
|
+
- **API keys don’t scale**
|
|
47
|
+
You can’t safely issue and manage API keys for every website on the internet.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## The Solution
|
|
52
|
+
|
|
53
|
+
ValleyDam uses **Ed25519 digital signatures** anchored in **DNS TXT records** to create a verifiable, spoof-resistant identity for bots.
|
|
54
|
+
|
|
55
|
+
### How it works
|
|
56
|
+
|
|
57
|
+
1. **The bot signs each request** using a private Ed25519 key.
|
|
58
|
+
2. **The server retrieves the public key** from the bot’s DNS record
|
|
59
|
+
(e.g. `_agent.yourwebsite.com`).
|
|
60
|
+
3. **The signature is verified**. If it matches, the bot’s identity is cryptographically proven.
|
|
61
|
+
|
|
62
|
+
No central authority. No shared secrets. No API keys.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 📦 Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install valleydam
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 🚀 Usage
|
|
75
|
+
|
|
76
|
+
### For Web Scrapper or Agent Developers (The Client)
|
|
77
|
+
|
|
78
|
+
If you are building a scraper or AI agent, use `ValleyDamSession` to automatically sign outgoing HTTP requests.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
#### 1. Generate Your Identity
|
|
83
|
+
|
|
84
|
+
Run the CLI to generate a private key and receive your DNS TXT record value:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
valleydam-gen
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Follow the printed instructions to add the TXT record to your domain’s DNS.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
#### 2. Use ValleyDam in Your Code
|
|
95
|
+
|
|
96
|
+
ValleyDam behaves just like the standard Python `requests` library.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from valleydam import ValleyDamSession
|
|
100
|
+
|
|
101
|
+
# Initialize your authenticated session
|
|
102
|
+
agent = ValleyDamSession(
|
|
103
|
+
domain="yourwebsite.com", # Your verified domain
|
|
104
|
+
private_key_path="yourwebsite_com_private.pem" # Generated in step 1
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Make requests as normal — they are now cryptographically signed
|
|
108
|
+
response = agent.get("https://protected-website.com/api/data")
|
|
109
|
+
|
|
110
|
+
print(response.text)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### For Website Owners (The Server)
|
|
116
|
+
|
|
117
|
+
Use theGuide
|
|
118
|
+
|
|
119
|
+
ValleyDam verifies incoming automated traffic and prevents agent impersonation by validating request signatures against DNS-published public keys.
|
|
120
|
+
|
|
121
|
+
It runs as middleware and works with Flask, Django, FastAPI, and similar frameworks.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
#### 🔒 Hard Validation (Block)
|
|
127
|
+
|
|
128
|
+
Reject invalid or spoofed requests. Best for protected or agent-only APIs.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from flask import Flask, request, jsonify
|
|
132
|
+
from valleydam import verify_request
|
|
133
|
+
|
|
134
|
+
app = Flask(__name__)
|
|
135
|
+
|
|
136
|
+
@app.route('/agent-api', methods=['POST'])
|
|
137
|
+
def protected_route():
|
|
138
|
+
try:
|
|
139
|
+
verify_request(request)
|
|
140
|
+
identity = request.headers.get('X-ValleyDam-KeyID')
|
|
141
|
+
return jsonify({
|
|
142
|
+
"status": "Welcome",
|
|
143
|
+
"verified_user": identity
|
|
144
|
+
})
|
|
145
|
+
except ValueError as e:
|
|
146
|
+
return jsonify({
|
|
147
|
+
"error": "Access Denied",
|
|
148
|
+
"reason": str(e)
|
|
149
|
+
}), 403
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
app.run(port=5000)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### 📄 Soft Validation (Log Only)
|
|
156
|
+
|
|
157
|
+
Attempt verification, log results, but allow all traffic.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import logging
|
|
161
|
+
from flask import Flask, request, jsonify
|
|
162
|
+
from valleydam import verify_request
|
|
163
|
+
|
|
164
|
+
app = Flask(__name__)
|
|
165
|
+
logging.basicConfig(level=logging.INFO)
|
|
166
|
+
|
|
167
|
+
@app.route('/public-api', methods=['GET', 'POST'])
|
|
168
|
+
def public_route():
|
|
169
|
+
identity = "Unverified (Anonymous)"
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
verify_request(request)
|
|
173
|
+
identity = request.headers.get('X-ValleyDam-KeyID')
|
|
174
|
+
logging.info(f"Verified request from: {identity}")
|
|
175
|
+
except ValueError as e:
|
|
176
|
+
logging.warning(f"Verification failed: {e}")
|
|
177
|
+
|
|
178
|
+
return jsonify({
|
|
179
|
+
"data": "This is public data",
|
|
180
|
+
"your_status": identity
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
app.run(port=5000)
|
|
185
|
+
```
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# ⛰️ ValleyDam
|
|
2
|
+
|
|
3
|
+
ValleyDam is a lightweight, open protocol for verifying the identity of AI agents and web scrapers using **DNS-backed cryptographic proof**.
|
|
4
|
+
|
|
5
|
+
It enables a website to verify that a request *actually* came from `bot.openai.com` (or your startup’s domain) **without** API keys, IP allowlists, or complex authentication handshakes.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The Problem
|
|
10
|
+
|
|
11
|
+
Today, websites have no reliable way to identify automated clients.
|
|
12
|
+
|
|
13
|
+
- **User-Agent strings are lies**
|
|
14
|
+
Anyone can send `User-Agent: Googlebot`.
|
|
15
|
+
|
|
16
|
+
- **IP blocking is messy**
|
|
17
|
+
Legitimate bots often run on shared cloud infrastructure (AWS, GCP).
|
|
18
|
+
|
|
19
|
+
- **API keys don’t scale**
|
|
20
|
+
You can’t safely issue and manage API keys for every website on the internet.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## The Solution
|
|
25
|
+
|
|
26
|
+
ValleyDam uses **Ed25519 digital signatures** anchored in **DNS TXT records** to create a verifiable, spoof-resistant identity for bots.
|
|
27
|
+
|
|
28
|
+
### How it works
|
|
29
|
+
|
|
30
|
+
1. **The bot signs each request** using a private Ed25519 key.
|
|
31
|
+
2. **The server retrieves the public key** from the bot’s DNS record
|
|
32
|
+
(e.g. `_agent.yourwebsite.com`).
|
|
33
|
+
3. **The signature is verified**. If it matches, the bot’s identity is cryptographically proven.
|
|
34
|
+
|
|
35
|
+
No central authority. No shared secrets. No API keys.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 📦 Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install valleydam
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 🚀 Usage
|
|
48
|
+
|
|
49
|
+
### For Web Scrapper or Agent Developers (The Client)
|
|
50
|
+
|
|
51
|
+
If you are building a scraper or AI agent, use `ValleyDamSession` to automatically sign outgoing HTTP requests.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
#### 1. Generate Your Identity
|
|
56
|
+
|
|
57
|
+
Run the CLI to generate a private key and receive your DNS TXT record value:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
valleydam-gen
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Follow the printed instructions to add the TXT record to your domain’s DNS.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
#### 2. Use ValleyDam in Your Code
|
|
68
|
+
|
|
69
|
+
ValleyDam behaves just like the standard Python `requests` library.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from valleydam import ValleyDamSession
|
|
73
|
+
|
|
74
|
+
# Initialize your authenticated session
|
|
75
|
+
agent = ValleyDamSession(
|
|
76
|
+
domain="yourwebsite.com", # Your verified domain
|
|
77
|
+
private_key_path="yourwebsite_com_private.pem" # Generated in step 1
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Make requests as normal — they are now cryptographically signed
|
|
81
|
+
response = agent.get("https://protected-website.com/api/data")
|
|
82
|
+
|
|
83
|
+
print(response.text)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### For Website Owners (The Server)
|
|
89
|
+
|
|
90
|
+
Use theGuide
|
|
91
|
+
|
|
92
|
+
ValleyDam verifies incoming automated traffic and prevents agent impersonation by validating request signatures against DNS-published public keys.
|
|
93
|
+
|
|
94
|
+
It runs as middleware and works with Flask, Django, FastAPI, and similar frameworks.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
#### 🔒 Hard Validation (Block)
|
|
100
|
+
|
|
101
|
+
Reject invalid or spoofed requests. Best for protected or agent-only APIs.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from flask import Flask, request, jsonify
|
|
105
|
+
from valleydam import verify_request
|
|
106
|
+
|
|
107
|
+
app = Flask(__name__)
|
|
108
|
+
|
|
109
|
+
@app.route('/agent-api', methods=['POST'])
|
|
110
|
+
def protected_route():
|
|
111
|
+
try:
|
|
112
|
+
verify_request(request)
|
|
113
|
+
identity = request.headers.get('X-ValleyDam-KeyID')
|
|
114
|
+
return jsonify({
|
|
115
|
+
"status": "Welcome",
|
|
116
|
+
"verified_user": identity
|
|
117
|
+
})
|
|
118
|
+
except ValueError as e:
|
|
119
|
+
return jsonify({
|
|
120
|
+
"error": "Access Denied",
|
|
121
|
+
"reason": str(e)
|
|
122
|
+
}), 403
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
app.run(port=5000)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### 📄 Soft Validation (Log Only)
|
|
129
|
+
|
|
130
|
+
Attempt verification, log results, but allow all traffic.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import logging
|
|
134
|
+
from flask import Flask, request, jsonify
|
|
135
|
+
from valleydam import verify_request
|
|
136
|
+
|
|
137
|
+
app = Flask(__name__)
|
|
138
|
+
logging.basicConfig(level=logging.INFO)
|
|
139
|
+
|
|
140
|
+
@app.route('/public-api', methods=['GET', 'POST'])
|
|
141
|
+
def public_route():
|
|
142
|
+
identity = "Unverified (Anonymous)"
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
verify_request(request)
|
|
146
|
+
identity = request.headers.get('X-ValleyDam-KeyID')
|
|
147
|
+
logging.info(f"Verified request from: {identity}")
|
|
148
|
+
except ValueError as e:
|
|
149
|
+
logging.warning(f"Verification failed: {e}")
|
|
150
|
+
|
|
151
|
+
return jsonify({
|
|
152
|
+
"data": "This is public data",
|
|
153
|
+
"your_status": identity
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
app.run(port=5000)
|
|
158
|
+
```
|
valleydam-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="valleydam",
|
|
8
|
+
version="0.1.0",
|
|
9
|
+
author="Supra N.",
|
|
10
|
+
description="A DNS-based cryptographic identity verification protocol for AI Agents.",
|
|
11
|
+
long_description=long_description,
|
|
12
|
+
long_description_content_type="text/markdown",
|
|
13
|
+
url="https://github.com/supra-nlpn/valley-dam",
|
|
14
|
+
project_urls={
|
|
15
|
+
"Bug Tracker": "https://github.com/supra-nlpn/valley-dam/issues",
|
|
16
|
+
},
|
|
17
|
+
classifiers=[
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Topic :: Security",
|
|
22
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
23
|
+
],
|
|
24
|
+
package_dir={"": "src"},
|
|
25
|
+
packages=find_packages(where="src"),
|
|
26
|
+
python_requires=">=3.7",
|
|
27
|
+
install_requires=[
|
|
28
|
+
"requests>=2.25.0",
|
|
29
|
+
"cryptography>=3.4.0",
|
|
30
|
+
"dnspython>=2.1.0",
|
|
31
|
+
],
|
|
32
|
+
entry_points={
|
|
33
|
+
'console_scripts': [
|
|
34
|
+
'valleydam-gen=valleydam.cli:main',
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ValleyDam: DNS-based Identity Verification Protocol.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import ValleyDamSession
|
|
6
|
+
from .verifier import verify_request, DnsKeyResolver
|
|
7
|
+
from .core import ValleyDamSigner, ValleyDamVerifier
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ValleyDamSession",
|
|
12
|
+
"verify_request",
|
|
13
|
+
"DnsKeyResolver",
|
|
14
|
+
"ValleyDamSigner",
|
|
15
|
+
"ValleyDamVerifier"
|
|
16
|
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import base64
|
|
3
|
+
import os
|
|
4
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
print("\n" + "="*50)
|
|
9
|
+
print(" ⛰️ VALLEYDAM IDENTITY GENERATOR")
|
|
10
|
+
print("="*50)
|
|
11
|
+
print("This tool will create a cryptographic identity for your AI Agent.\n")
|
|
12
|
+
|
|
13
|
+
# 1. Get Domain
|
|
14
|
+
domain = input("Enter your Agent's Domain (e.g., bot.mysite.com): ").strip()
|
|
15
|
+
if not domain:
|
|
16
|
+
print("❌ Error: Domain is required.")
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
# 2. Generate Keys
|
|
20
|
+
print(f"\nGenerating Ed25519 keypair for {domain}...")
|
|
21
|
+
private_key = ed25519.Ed25519PrivateKey.generate()
|
|
22
|
+
public_key = private_key.public_key()
|
|
23
|
+
|
|
24
|
+
# 3. Save Private Key
|
|
25
|
+
safe_name = domain.replace('.', '_')
|
|
26
|
+
filename = f"{safe_name}_private.pem"
|
|
27
|
+
|
|
28
|
+
if os.path.exists(filename):
|
|
29
|
+
overwrite = input(f"⚠️ File {filename} exists. Overwrite? (y/N): ")
|
|
30
|
+
if overwrite.lower() != 'y':
|
|
31
|
+
print("Aborted.")
|
|
32
|
+
sys.exit(0)
|
|
33
|
+
|
|
34
|
+
private_bytes = private_key.private_bytes(
|
|
35
|
+
encoding=serialization.Encoding.PEM,
|
|
36
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
37
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
with open(filename, "wb") as f:
|
|
41
|
+
f.write(private_bytes)
|
|
42
|
+
|
|
43
|
+
# 4. Format Public Key for DNS
|
|
44
|
+
public_bytes = public_key.public_bytes(
|
|
45
|
+
encoding=serialization.Encoding.Raw,
|
|
46
|
+
format=serialization.PublicFormat.Raw
|
|
47
|
+
)
|
|
48
|
+
public_b64 = base64.b64encode(public_bytes).decode('utf-8')
|
|
49
|
+
|
|
50
|
+
# 5. Output Instructions
|
|
51
|
+
print(f"\n✅ SUCCESS! Private key saved to: {os.path.abspath(filename)}")
|
|
52
|
+
print(" (DO NOT share this file. Add it to your .gitignore)")
|
|
53
|
+
|
|
54
|
+
print("\n🌍 === DNS SETUP INSTRUCTIONS ===")
|
|
55
|
+
print(f"Log in to your DNS provider for '{domain}' and add this TXT record:\n")
|
|
56
|
+
print(f" Host: _agent")
|
|
57
|
+
print(f" Value: v=vd1; k=ed25519; p={public_b64};\n")
|
|
58
|
+
print("="*50 + "\n")
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from .core import ValleyDamSigner
|
|
5
|
+
|
|
6
|
+
class ValleyDamSession(requests.Session):
|
|
7
|
+
"""
|
|
8
|
+
A Requests Session that automatically signs every outgoing request
|
|
9
|
+
with a ValleyDam Identity.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, domain: str, private_key_path: str):
|
|
13
|
+
super().__init__()
|
|
14
|
+
self.domain = domain
|
|
15
|
+
self.signer = ValleyDamSigner(private_key_path)
|
|
16
|
+
|
|
17
|
+
def request(self, method: str, url: str, *args, **kwargs):
|
|
18
|
+
"""
|
|
19
|
+
Overrides the standard request method to inject authentication headers.
|
|
20
|
+
"""
|
|
21
|
+
# Parse URL to get signing components
|
|
22
|
+
parsed = urlparse(url)
|
|
23
|
+
host = parsed.netloc
|
|
24
|
+
path = parsed.path if parsed.path else "/"
|
|
25
|
+
|
|
26
|
+
# Generate Signature
|
|
27
|
+
headers = self.signer.sign(method, host, path)
|
|
28
|
+
|
|
29
|
+
# Add Identity Pointer
|
|
30
|
+
headers['X-ValleyDam-KeyID'] = f"dns:{self.domain}"
|
|
31
|
+
|
|
32
|
+
# Merge with user-provided headers
|
|
33
|
+
if 'headers' not in kwargs:
|
|
34
|
+
kwargs['headers'] = {}
|
|
35
|
+
kwargs['headers'].update(headers)
|
|
36
|
+
|
|
37
|
+
return super().request(method, url, *args, **kwargs)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import time
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
|
+
|
|
7
|
+
# Protocol Constants
|
|
8
|
+
SIGNATURE_ALGORITHM = 'ed25519'
|
|
9
|
+
MAX_TIME_SKEW_SECONDS = 30
|
|
10
|
+
|
|
11
|
+
class ValleyDamSigner:
|
|
12
|
+
"""Handles the cryptographic signing of HTTP requests."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, private_key_path: str):
|
|
15
|
+
"""
|
|
16
|
+
Load an Ed25519 private key from a PEM file.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
private_key_path: Path to the .pem file generated by the CLI.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
with open(private_key_path, 'rb') as key_file:
|
|
23
|
+
self.private_key = serialization.load_pem_private_key(
|
|
24
|
+
key_file.read(),
|
|
25
|
+
password=None
|
|
26
|
+
)
|
|
27
|
+
except FileNotFoundError:
|
|
28
|
+
raise ValueError(f"Private key not found at: {private_key_path}")
|
|
29
|
+
except ValueError:
|
|
30
|
+
raise ValueError("Invalid PEM file format.")
|
|
31
|
+
|
|
32
|
+
def sign(self, method: str, host: str, path: str) -> Dict[str, str]:
|
|
33
|
+
"""
|
|
34
|
+
Generate authentication headers for a request.
|
|
35
|
+
|
|
36
|
+
The payload format is: "{METHOD} {HOST} {PATH} {TIMESTAMP}"
|
|
37
|
+
"""
|
|
38
|
+
timestamp = int(time.time())
|
|
39
|
+
|
|
40
|
+
# Canonicalization: Create the immutable string to sign
|
|
41
|
+
payload = f"{method.upper()} {host} {path} {timestamp}".encode('utf-8')
|
|
42
|
+
|
|
43
|
+
# Sign using Ed25519
|
|
44
|
+
sig_bytes = self.private_key.sign(payload)
|
|
45
|
+
sig_b64 = base64.b64encode(sig_bytes).decode('utf-8')
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
'X-ValleyDam-Signature': sig_b64,
|
|
49
|
+
'X-ValleyDam-Time': str(timestamp),
|
|
50
|
+
'X-ValleyDam-Method': SIGNATURE_ALGORITHM
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ValleyDamVerifier:
|
|
55
|
+
"""Handles the verification of incoming request signatures."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, key_resolver_func):
|
|
58
|
+
"""
|
|
59
|
+
Args:
|
|
60
|
+
key_resolver_func: A callable that takes a key_id string and
|
|
61
|
+
returns an Ed25519PublicKey object (or None).
|
|
62
|
+
"""
|
|
63
|
+
self.resolve_key = key_resolver_func
|
|
64
|
+
|
|
65
|
+
def verify(self, method: str, host: str, path: str, headers: Dict) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Verify that a request was signed by the owner of the DNS record.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: If headers are missing, timestamp is expired, or signature is invalid.
|
|
71
|
+
"""
|
|
72
|
+
# 1. Extract Headers
|
|
73
|
+
sig_b64 = headers.get('X-ValleyDam-Signature')
|
|
74
|
+
timestamp = headers.get('X-ValleyDam-Time')
|
|
75
|
+
key_id = headers.get('X-ValleyDam-KeyID')
|
|
76
|
+
|
|
77
|
+
if not all([sig_b64, timestamp, key_id]):
|
|
78
|
+
raise ValueError("Missing required ValleyDam authentication headers.")
|
|
79
|
+
|
|
80
|
+
# 2. Check Timestamp (Replay Attack Prevention)
|
|
81
|
+
try:
|
|
82
|
+
req_time = int(timestamp)
|
|
83
|
+
now = int(time.time())
|
|
84
|
+
if abs(now - req_time) > MAX_TIME_SKEW_SECONDS:
|
|
85
|
+
raise ValueError(f"Request expired. Server time: {now}, Request time: {req_time}")
|
|
86
|
+
except ValueError:
|
|
87
|
+
raise ValueError("Invalid timestamp format.")
|
|
88
|
+
|
|
89
|
+
# 3. Resolve Public Key
|
|
90
|
+
public_key = self.resolve_key(key_id)
|
|
91
|
+
if not public_key:
|
|
92
|
+
raise ValueError(f"Public key not found for identity: {key_id}")
|
|
93
|
+
|
|
94
|
+
# 4. Reconstruct and Verify Payload
|
|
95
|
+
payload = f"{method.upper()} {host} {path} {timestamp}".encode('utf-8')
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
sig_bytes = base64.b64decode(sig_b64)
|
|
99
|
+
public_key.verify(sig_bytes, payload)
|
|
100
|
+
return True
|
|
101
|
+
except Exception:
|
|
102
|
+
raise ValueError("Cryptographic verification failed. Signature does not match payload.")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import dns.resolver
|
|
2
|
+
import base64
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
6
|
+
from .core import ValleyDamVerifier
|
|
7
|
+
|
|
8
|
+
# --- CACHED LOOKUP FUNCTION ---
|
|
9
|
+
@lru_cache(maxsize=256)
|
|
10
|
+
def _fetch_dns_record(key_id: str) -> Optional[ed25519.Ed25519PublicKey]:
|
|
11
|
+
"""
|
|
12
|
+
Fetch and cache the Ed25519 public key from a DNS TXT record.
|
|
13
|
+
Format expected: v=vd1; k=ed25519; p=<BASE64_KEY>;
|
|
14
|
+
"""
|
|
15
|
+
if not key_id.startswith("dns:"):
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
domain = key_id.split(":", 1)[1]
|
|
20
|
+
except IndexError:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
# Use Google and Cloudflare DNS to avoid local ISP caching issues
|
|
24
|
+
resolver = dns.resolver.Resolver(configure=False)
|
|
25
|
+
resolver.nameservers = ['8.8.8.8', '1.1.1.1']
|
|
26
|
+
resolver.lifetime = 2.0 # Timeout after 2 seconds
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
txt_target = f"_agent.{domain}"
|
|
30
|
+
answers = resolver.resolve(txt_target, 'TXT')
|
|
31
|
+
|
|
32
|
+
for rdata in answers:
|
|
33
|
+
# Clean up the TXT record string
|
|
34
|
+
txt_string = rdata.to_text().replace('"', '').strip()
|
|
35
|
+
|
|
36
|
+
# Parse key-value pairs
|
|
37
|
+
parts = {}
|
|
38
|
+
for item in txt_string.split(';'):
|
|
39
|
+
if '=' in item:
|
|
40
|
+
k, v = item.strip().split('=', 1)
|
|
41
|
+
parts[k] = v
|
|
42
|
+
|
|
43
|
+
# Return the key if found
|
|
44
|
+
if parts.get('p'):
|
|
45
|
+
try:
|
|
46
|
+
pub_bytes = base64.b64decode(parts['p'])
|
|
47
|
+
return ed25519.Ed25519PublicKey.from_public_bytes(pub_bytes)
|
|
48
|
+
except Exception:
|
|
49
|
+
continue # Malformed key, try next record
|
|
50
|
+
|
|
51
|
+
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.exception.Timeout):
|
|
52
|
+
return None
|
|
53
|
+
except Exception as e:
|
|
54
|
+
# In production, you might want to log this error
|
|
55
|
+
# print(f"DNS Error: {e}")
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DnsKeyResolver:
|
|
62
|
+
"""Wrapper class for the cached DNS lookup."""
|
|
63
|
+
def resolve(self, key_id: str):
|
|
64
|
+
return _fetch_dns_record(key_id)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Singleton instance to maintain cache across requests
|
|
68
|
+
GLOBAL_RESOLVER = DnsKeyResolver()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def verify_request(request) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Helper function for Flask/Django/FastAPI.
|
|
74
|
+
|
|
75
|
+
Usage:
|
|
76
|
+
try:
|
|
77
|
+
verify_request(request)
|
|
78
|
+
except ValueError as e:
|
|
79
|
+
abort(403, str(e))
|
|
80
|
+
"""
|
|
81
|
+
verifier = ValleyDamVerifier(GLOBAL_RESOLVER.resolve)
|
|
82
|
+
|
|
83
|
+
# Handle Flask-style request objects
|
|
84
|
+
host = request.host
|
|
85
|
+
path = request.path
|
|
86
|
+
|
|
87
|
+
return verifier.verify(request.method, host, path, request.headers)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: valleydam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A DNS-based cryptographic identity verification protocol for AI Agents.
|
|
5
|
+
Home-page: https://github.com/supra-nlpn/valley-dam
|
|
6
|
+
Author: Supra N.
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/supra-nlpn/valley-dam/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests>=2.25.0
|
|
16
|
+
Requires-Dist: cryptography>=3.4.0
|
|
17
|
+
Requires-Dist: dnspython>=2.1.0
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: project-url
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
# ⛰️ ValleyDam
|
|
29
|
+
|
|
30
|
+
ValleyDam is a lightweight, open protocol for verifying the identity of AI agents and web scrapers using **DNS-backed cryptographic proof**.
|
|
31
|
+
|
|
32
|
+
It enables a website to verify that a request *actually* came from `bot.openai.com` (or your startup’s domain) **without** API keys, IP allowlists, or complex authentication handshakes.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## The Problem
|
|
37
|
+
|
|
38
|
+
Today, websites have no reliable way to identify automated clients.
|
|
39
|
+
|
|
40
|
+
- **User-Agent strings are lies**
|
|
41
|
+
Anyone can send `User-Agent: Googlebot`.
|
|
42
|
+
|
|
43
|
+
- **IP blocking is messy**
|
|
44
|
+
Legitimate bots often run on shared cloud infrastructure (AWS, GCP).
|
|
45
|
+
|
|
46
|
+
- **API keys don’t scale**
|
|
47
|
+
You can’t safely issue and manage API keys for every website on the internet.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## The Solution
|
|
52
|
+
|
|
53
|
+
ValleyDam uses **Ed25519 digital signatures** anchored in **DNS TXT records** to create a verifiable, spoof-resistant identity for bots.
|
|
54
|
+
|
|
55
|
+
### How it works
|
|
56
|
+
|
|
57
|
+
1. **The bot signs each request** using a private Ed25519 key.
|
|
58
|
+
2. **The server retrieves the public key** from the bot’s DNS record
|
|
59
|
+
(e.g. `_agent.yourwebsite.com`).
|
|
60
|
+
3. **The signature is verified**. If it matches, the bot’s identity is cryptographically proven.
|
|
61
|
+
|
|
62
|
+
No central authority. No shared secrets. No API keys.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 📦 Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install valleydam
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 🚀 Usage
|
|
75
|
+
|
|
76
|
+
### For Web Scrapper or Agent Developers (The Client)
|
|
77
|
+
|
|
78
|
+
If you are building a scraper or AI agent, use `ValleyDamSession` to automatically sign outgoing HTTP requests.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
#### 1. Generate Your Identity
|
|
83
|
+
|
|
84
|
+
Run the CLI to generate a private key and receive your DNS TXT record value:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
valleydam-gen
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Follow the printed instructions to add the TXT record to your domain’s DNS.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
#### 2. Use ValleyDam in Your Code
|
|
95
|
+
|
|
96
|
+
ValleyDam behaves just like the standard Python `requests` library.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from valleydam import ValleyDamSession
|
|
100
|
+
|
|
101
|
+
# Initialize your authenticated session
|
|
102
|
+
agent = ValleyDamSession(
|
|
103
|
+
domain="yourwebsite.com", # Your verified domain
|
|
104
|
+
private_key_path="yourwebsite_com_private.pem" # Generated in step 1
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Make requests as normal — they are now cryptographically signed
|
|
108
|
+
response = agent.get("https://protected-website.com/api/data")
|
|
109
|
+
|
|
110
|
+
print(response.text)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### For Website Owners (The Server)
|
|
116
|
+
|
|
117
|
+
Use theGuide
|
|
118
|
+
|
|
119
|
+
ValleyDam verifies incoming automated traffic and prevents agent impersonation by validating request signatures against DNS-published public keys.
|
|
120
|
+
|
|
121
|
+
It runs as middleware and works with Flask, Django, FastAPI, and similar frameworks.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
#### 🔒 Hard Validation (Block)
|
|
127
|
+
|
|
128
|
+
Reject invalid or spoofed requests. Best for protected or agent-only APIs.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from flask import Flask, request, jsonify
|
|
132
|
+
from valleydam import verify_request
|
|
133
|
+
|
|
134
|
+
app = Flask(__name__)
|
|
135
|
+
|
|
136
|
+
@app.route('/agent-api', methods=['POST'])
|
|
137
|
+
def protected_route():
|
|
138
|
+
try:
|
|
139
|
+
verify_request(request)
|
|
140
|
+
identity = request.headers.get('X-ValleyDam-KeyID')
|
|
141
|
+
return jsonify({
|
|
142
|
+
"status": "Welcome",
|
|
143
|
+
"verified_user": identity
|
|
144
|
+
})
|
|
145
|
+
except ValueError as e:
|
|
146
|
+
return jsonify({
|
|
147
|
+
"error": "Access Denied",
|
|
148
|
+
"reason": str(e)
|
|
149
|
+
}), 403
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
app.run(port=5000)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### 📄 Soft Validation (Log Only)
|
|
156
|
+
|
|
157
|
+
Attempt verification, log results, but allow all traffic.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import logging
|
|
161
|
+
from flask import Flask, request, jsonify
|
|
162
|
+
from valleydam import verify_request
|
|
163
|
+
|
|
164
|
+
app = Flask(__name__)
|
|
165
|
+
logging.basicConfig(level=logging.INFO)
|
|
166
|
+
|
|
167
|
+
@app.route('/public-api', methods=['GET', 'POST'])
|
|
168
|
+
def public_route():
|
|
169
|
+
identity = "Unverified (Anonymous)"
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
verify_request(request)
|
|
173
|
+
identity = request.headers.get('X-ValleyDam-KeyID')
|
|
174
|
+
logging.info(f"Verified request from: {identity}")
|
|
175
|
+
except ValueError as e:
|
|
176
|
+
logging.warning(f"Verification failed: {e}")
|
|
177
|
+
|
|
178
|
+
return jsonify({
|
|
179
|
+
"data": "This is public data",
|
|
180
|
+
"your_status": identity
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
app.run(port=5000)
|
|
185
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
src/valleydam/__init__.py
|
|
4
|
+
src/valleydam/cli.py
|
|
5
|
+
src/valleydam/client.py
|
|
6
|
+
src/valleydam/core.py
|
|
7
|
+
src/valleydam/verifier.py
|
|
8
|
+
src/valleydam.egg-info/PKG-INFO
|
|
9
|
+
src/valleydam.egg-info/SOURCES.txt
|
|
10
|
+
src/valleydam.egg-info/dependency_links.txt
|
|
11
|
+
src/valleydam.egg-info/entry_points.txt
|
|
12
|
+
src/valleydam.egg-info/requires.txt
|
|
13
|
+
src/valleydam.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
valleydam
|