creduent 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.
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: creduent
3
+ Version: 0.1.0
4
+ Summary: Creduent Protocol SDK — cryptographic identity for AI agents
5
+ License: MIT
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: cryptography
12
+ Requires-Dist: requests
13
+ Requires-Dist: jcs
14
+ Requires-Dist: fastapi
15
+
16
+ # Creduent Python SDK
17
+
18
+ The official Python SDK for the **Creduent Protocol** — a federated, open trust-verification layer and cryptographic identity infrastructure for autonomous AI agents.
19
+
20
+ ## Installation
21
+
22
+ Install the self-contained package from PyPI with a single command:
23
+
24
+ ```bash
25
+ pip install creduent
26
+ ```
27
+
28
+ ## Quickstart
29
+
30
+ Below is a complete quickstart demonstrating keypair generation, self-signing, verification, registration, and attestation resolution using the SDK.
31
+
32
+ ```python
33
+ from creduent import (
34
+ generate_keys,
35
+ sign,
36
+ verify,
37
+ register,
38
+ attest,
39
+ CreduEntError
40
+ )
41
+
42
+ try:
43
+ # 1. Generate a new Ed25519 keypair
44
+ private_key_pem, public_key_str = generate_keys()
45
+ print(f"Generated Public Key: {public_key_str}\n")
46
+
47
+ # 2. Sign a draft agent.json document
48
+ draft_document = {
49
+ "agent_id": "agent://creduent/reconbot",
50
+ "owner": "Creduent",
51
+ "public_key": public_key_str,
52
+ "endpoint": "https://api.idevsec.com/recon",
53
+ "capabilities": ["osint", "dns_lookup", "vulnerability_scan"]
54
+ }
55
+
56
+ signed_doc = sign(draft_document, private_key_pem)
57
+ print("Signed agent.json:")
58
+ print(signed_doc)
59
+ print()
60
+
61
+ # 3. Verify a self-signed agent.json (from dict, URL, domain, or agent:// URI)
62
+ # Verification with a document dictionary
63
+ result = verify(signed_doc)
64
+ print(f"Self-Signed Verification Result (dict): {result.valid}")
65
+
66
+ # Verification with a live agent URL
67
+ live_result = verify("https://api.idevsec.com/.well-known/agent.json")
68
+ print(f"Live Verification Result (URL): {live_result.valid}")
69
+ print(f"Live Agent ID: {live_result.agent_id}")
70
+ print(f"Live Capabilities: {live_result.capabilities}\n")
71
+
72
+ # 4. Register an agent with the Creduent registry
73
+ # Returns RegisterResult with success (bool), attestation (dict|None), error (str|None)
74
+ reg_result = register(
75
+ agent_id="agent://creduent/reconbot",
76
+ domain="api.idevsec.com",
77
+ agent_json_url="https://api.idevsec.com/.well-known/agent.json"
78
+ )
79
+ print(f"Registration Successful: {reg_result.success}")
80
+ print(f"Attestation: {reg_result.attestation}\n")
81
+
82
+ # 5. Fetch and validate an active attestation for an agent
83
+ # Returns AttestResult with attested (bool), level (str), issued_at, expires_at, error (str|None)
84
+ attest_result = attest("agent://creduent/reconbot")
85
+ print(f"Is Attested: {attest_result.attested}")
86
+ print(f"Attestation Level: {attest_result.level}")
87
+ print(f"Issued At: {attest_result.issued_at}")
88
+ print(f"Expires At: {attest_result.expires_at}\n")
89
+
90
+ except CreduEntError as e:
91
+ print(f"Creduent Protocol Error occurred: {e}")
92
+ ```
93
+
94
+ ## Protocol Specification
95
+
96
+ For full information on the cryptographic standards, JCS canonicalization, and the federated verification workflows, read the complete [Creduent Protocol Specification](https://github.com/cyberfascinate/creduent).
@@ -0,0 +1,81 @@
1
+ # Creduent Python SDK
2
+
3
+ The official Python SDK for the **Creduent Protocol** — a federated, open trust-verification layer and cryptographic identity infrastructure for autonomous AI agents.
4
+
5
+ ## Installation
6
+
7
+ Install the self-contained package from PyPI with a single command:
8
+
9
+ ```bash
10
+ pip install creduent
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ Below is a complete quickstart demonstrating keypair generation, self-signing, verification, registration, and attestation resolution using the SDK.
16
+
17
+ ```python
18
+ from creduent import (
19
+ generate_keys,
20
+ sign,
21
+ verify,
22
+ register,
23
+ attest,
24
+ CreduEntError
25
+ )
26
+
27
+ try:
28
+ # 1. Generate a new Ed25519 keypair
29
+ private_key_pem, public_key_str = generate_keys()
30
+ print(f"Generated Public Key: {public_key_str}\n")
31
+
32
+ # 2. Sign a draft agent.json document
33
+ draft_document = {
34
+ "agent_id": "agent://creduent/reconbot",
35
+ "owner": "Creduent",
36
+ "public_key": public_key_str,
37
+ "endpoint": "https://api.idevsec.com/recon",
38
+ "capabilities": ["osint", "dns_lookup", "vulnerability_scan"]
39
+ }
40
+
41
+ signed_doc = sign(draft_document, private_key_pem)
42
+ print("Signed agent.json:")
43
+ print(signed_doc)
44
+ print()
45
+
46
+ # 3. Verify a self-signed agent.json (from dict, URL, domain, or agent:// URI)
47
+ # Verification with a document dictionary
48
+ result = verify(signed_doc)
49
+ print(f"Self-Signed Verification Result (dict): {result.valid}")
50
+
51
+ # Verification with a live agent URL
52
+ live_result = verify("https://api.idevsec.com/.well-known/agent.json")
53
+ print(f"Live Verification Result (URL): {live_result.valid}")
54
+ print(f"Live Agent ID: {live_result.agent_id}")
55
+ print(f"Live Capabilities: {live_result.capabilities}\n")
56
+
57
+ # 4. Register an agent with the Creduent registry
58
+ # Returns RegisterResult with success (bool), attestation (dict|None), error (str|None)
59
+ reg_result = register(
60
+ agent_id="agent://creduent/reconbot",
61
+ domain="api.idevsec.com",
62
+ agent_json_url="https://api.idevsec.com/.well-known/agent.json"
63
+ )
64
+ print(f"Registration Successful: {reg_result.success}")
65
+ print(f"Attestation: {reg_result.attestation}\n")
66
+
67
+ # 5. Fetch and validate an active attestation for an agent
68
+ # Returns AttestResult with attested (bool), level (str), issued_at, expires_at, error (str|None)
69
+ attest_result = attest("agent://creduent/reconbot")
70
+ print(f"Is Attested: {attest_result.attested}")
71
+ print(f"Attestation Level: {attest_result.level}")
72
+ print(f"Issued At: {attest_result.issued_at}")
73
+ print(f"Expires At: {attest_result.expires_at}\n")
74
+
75
+ except CreduEntError as e:
76
+ print(f"Creduent Protocol Error occurred: {e}")
77
+ ```
78
+
79
+ ## Protocol Specification
80
+
81
+ For full information on the cryptographic standards, JCS canonicalization, and the federated verification workflows, read the complete [Creduent Protocol Specification](https://github.com/cyberfascinate/creduent).
@@ -0,0 +1,29 @@
1
+ """
2
+ Creduent Protocol SDK — Cryptographic identity verification for AI agents.
3
+ """
4
+
5
+ from creduent.sign import generate_keys, sign
6
+ from creduent.verify import verify, VerifyResult
7
+ from creduent.register import register, RegisterResult
8
+ from creduent.attest import attest, AttestResult
9
+ from creduent.exceptions import (
10
+ CreduEntError,
11
+ VerificationError,
12
+ RegistrationError,
13
+ AttestationError
14
+ )
15
+
16
+ __all__ = [
17
+ "generate_keys",
18
+ "sign",
19
+ "verify",
20
+ "VerifyResult",
21
+ "register",
22
+ "RegisterResult",
23
+ "attest",
24
+ "AttestResult",
25
+ "CreduEntError",
26
+ "VerificationError",
27
+ "RegistrationError",
28
+ "AttestationError"
29
+ ]
@@ -0,0 +1,80 @@
1
+ import requests
2
+ from dataclasses import dataclass
3
+ from typing import Optional
4
+
5
+ from creduent.exceptions import AttestationError
6
+
7
+ @dataclass
8
+ class AttestResult:
9
+ attested: bool
10
+ level: Optional[str]
11
+ issued_at: Optional[str]
12
+ expires_at: Optional[str]
13
+ error: Optional[str] = None
14
+
15
+ def attest(
16
+ agent_id: str,
17
+ registry_url: str = "https://api.idevsec.com"
18
+ ) -> AttestResult:
19
+ """
20
+ Fetch attestation for an agent from the Creduent registry.
21
+ Returns AttestResult.
22
+ Raises AttestationError on network, timeout, or registry 5xx failures.
23
+ """
24
+ base_url = registry_url.rstrip('/')
25
+
26
+ # URL escape agent_id if needed, but since it's a path parameter:
27
+ # FastAPI path parameter matches `/attest/{agent_id:path}`
28
+ # E.g. /registry/attest/agent://creduent/reconbot
29
+ # Note: double slashes in agent:// might get collapsed by some proxies or routers.
30
+ # The registry endpoint router handles path normalization.
31
+ if base_url.endswith('/registry'):
32
+ endpoint = f"{base_url}/attest/{agent_id}"
33
+ else:
34
+ endpoint = f"{base_url}/registry/attest/{agent_id}"
35
+
36
+ try:
37
+ response = requests.get(endpoint, timeout=10)
38
+ except requests.RequestException as e:
39
+ # Fallback without /registry if default failed
40
+ if not base_url.endswith('/registry'):
41
+ fallback_endpoint = f"{base_url}/attest/{agent_id}"
42
+ try:
43
+ response = requests.get(fallback_endpoint, timeout=10)
44
+ except requests.RequestException as inner_e:
45
+ raise AttestationError(f"Connection to Creduent registry failed: {inner_e}")
46
+ else:
47
+ raise AttestationError(f"Connection to Creduent registry failed: {e}")
48
+
49
+ if response.status_code == 200:
50
+ try:
51
+ data = response.json()
52
+ return AttestResult(
53
+ attested=True,
54
+ level=data.get("level", "verified"),
55
+ issued_at=data.get("issued_at"),
56
+ expires_at=data.get("expires_at"),
57
+ error=None
58
+ )
59
+ except Exception as e:
60
+ raise AttestationError(f"Registry returned invalid JSON: {e}")
61
+ elif response.status_code == 404:
62
+ # Agent is not registered/attested
63
+ return AttestResult(
64
+ attested=False,
65
+ level=None,
66
+ issued_at=None,
67
+ expires_at=None,
68
+ error="Attestation not found or expired"
69
+ )
70
+ else:
71
+ # Server or validation error
72
+ err_detail = response.text
73
+ try:
74
+ err_json = response.json()
75
+ if isinstance(err_json, dict) and "detail" in err_json:
76
+ err_detail = err_json["detail"]
77
+ except Exception:
78
+ pass
79
+
80
+ raise AttestationError(f"Attestation query failed with HTTP {response.status_code}: {err_detail}")
@@ -0,0 +1,9 @@
1
+ # NOTE: Sync with creduent/crypto.py
2
+ import jcs
3
+
4
+ def canonicalize(obj) -> str:
5
+ """
6
+ Canonicalize JSON object according to RFC 8785 (JCS) using the jcs library.
7
+ Returns a UTF-8 string.
8
+ """
9
+ return jcs.canonicalize(obj).decode('utf-8')
@@ -0,0 +1,19 @@
1
+ """
2
+ Creduent Python SDK custom exceptions.
3
+ """
4
+
5
+ class CreduEntError(Exception):
6
+ """Base exception for all Creduent errors."""
7
+ pass
8
+
9
+ class VerificationError(CreduEntError):
10
+ """Exception raised when identity or cryptographic verification fails."""
11
+ pass
12
+
13
+ class RegistrationError(CreduEntError):
14
+ """Exception raised when agent registration with the registry fails."""
15
+ pass
16
+
17
+ class AttestationError(CreduEntError):
18
+ """Exception raised when querying or validating registry attestations fails."""
19
+ pass
@@ -0,0 +1,78 @@
1
+ import json
2
+ import requests
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ from creduent.exceptions import RegistrationError
7
+
8
+ @dataclass
9
+ class RegisterResult:
10
+ success: bool
11
+ attestation: Optional[dict]
12
+ error: Optional[str] = None
13
+
14
+ def register(
15
+ agent_id: str,
16
+ domain: str,
17
+ agent_json_url: str,
18
+ registry_url: str = "https://api.idevsec.com"
19
+ ) -> RegisterResult:
20
+ """
21
+ Register agent with Creduent registry.
22
+ Returns RegisterResult.
23
+ Raises RegistrationError on network, timeout, or validation failure.
24
+ """
25
+ base_url = registry_url.rstrip('/')
26
+ # If URL already ends with /registry, the endpoint is /register.
27
+ # Otherwise, it's /registry/register.
28
+ if base_url.endswith('/registry'):
29
+ endpoint = f"{base_url}/register"
30
+ else:
31
+ endpoint = f"{base_url}/registry/register"
32
+
33
+ payload = {
34
+ "agent_id": agent_id,
35
+ "domain": domain,
36
+ "agent_json_url": agent_json_url
37
+ }
38
+
39
+ try:
40
+ response = requests.post(
41
+ endpoint,
42
+ json=payload,
43
+ headers={"Content-Type": "application/json"},
44
+ timeout=10
45
+ )
46
+ except requests.RequestException as e:
47
+ # If the preferred endpoint failed, try fallback /register directly
48
+ if not base_url.endswith('/registry'):
49
+ fallback_endpoint = f"{base_url}/register"
50
+ try:
51
+ response = requests.post(
52
+ fallback_endpoint,
53
+ json=payload,
54
+ headers={"Content-Type": "application/json"},
55
+ timeout=10
56
+ )
57
+ except requests.RequestException as inner_e:
58
+ raise RegistrationError(f"Connection to Creduent registry failed: {inner_e}")
59
+ else:
60
+ raise RegistrationError(f"Connection to Creduent registry failed: {e}")
61
+
62
+ if response.status_code == 200:
63
+ try:
64
+ attestation = response.json()
65
+ return RegisterResult(success=True, attestation=attestation, error=None)
66
+ except Exception as e:
67
+ raise RegistrationError(f"Registry returned invalid JSON: {e}")
68
+ else:
69
+ # Non-200 error
70
+ err_detail = response.text
71
+ try:
72
+ err_json = response.json()
73
+ if isinstance(err_json, dict) and "detail" in err_json:
74
+ err_detail = err_json["detail"]
75
+ except Exception:
76
+ pass
77
+
78
+ raise RegistrationError(f"Registration failed with HTTP {response.status_code}: {err_detail}")
@@ -0,0 +1,146 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import base64
5
+ import argparse
6
+ from datetime import datetime, timezone
7
+ from cryptography.hazmat.primitives.asymmetric import ed25519
8
+ from cryptography.hazmat.primitives import serialization
9
+ from creduent.crypto import canonicalize
10
+ from creduent.exceptions import CreduEntError
11
+
12
+ def generate_keys() -> tuple[str, str]:
13
+ """
14
+ Generate a new Ed25519 keypair.
15
+ Returns: (private_key_pem_string, "ed25519:<base64>" public_key_string)
16
+ """
17
+ try:
18
+ private_key = ed25519.Ed25519PrivateKey.generate()
19
+
20
+ # Format Private Key as PEM string
21
+ private_pem_bytes = private_key.private_bytes(
22
+ encoding=serialization.Encoding.PEM,
23
+ format=serialization.PrivateFormat.PKCS8,
24
+ encryption_algorithm=serialization.NoEncryption()
25
+ )
26
+ private_key_pem = private_pem_bytes.decode('utf-8')
27
+
28
+ # Extract and format Public Key
29
+ public_key = private_key.public_key()
30
+ public_bytes = public_key.public_bytes(
31
+ encoding=serialization.Encoding.Raw,
32
+ format=serialization.PublicFormat.Raw
33
+ )
34
+ public_b64 = base64.b64encode(public_bytes).decode('utf-8')
35
+ public_key_str = f"ed25519:{public_b64}"
36
+
37
+ return private_key_pem, public_key_str
38
+ except Exception as e:
39
+ raise CreduEntError(f"Failed to generate keys: {e}")
40
+
41
+ def sign(draft: dict, private_key_pem: str) -> dict:
42
+ """
43
+ Sign a draft agent.json dict and return the signed document.
44
+ Adds JCS canonicalization (RFC 8785) + Ed25519 signature field.
45
+ """
46
+ if not isinstance(draft, dict):
47
+ raise CreduEntError("Draft must be a dictionary")
48
+
49
+ doc = draft.copy()
50
+
51
+ # Normalize fields
52
+ doc["version"] = "1.0"
53
+ if "issued_at" not in doc:
54
+ doc["issued_at"] = datetime.now(timezone.utc).isoformat(timespec='seconds').replace("+00:00", "Z")
55
+
56
+ # Remove signature before signing
57
+ doc.pop("signature", None)
58
+
59
+ try:
60
+ # Load the Ed25519 private key
61
+ private_key = serialization.load_pem_private_key(
62
+ private_key_pem.encode('utf-8'),
63
+ password=None
64
+ )
65
+ if not isinstance(private_key, ed25519.Ed25519PrivateKey):
66
+ raise ValueError("Key is not an Ed25519 private key")
67
+ except Exception as e:
68
+ raise CreduEntError(f"Failed parsing private key PEM: {e}")
69
+
70
+ try:
71
+ # JCS canonicalization
72
+ canonical_str = canonicalize(doc)
73
+ canonical_bytes = canonical_str.encode('utf-8')
74
+
75
+ # Sign payload
76
+ signature_bytes = private_key.sign(canonical_bytes)
77
+ signature_b64 = base64.b64encode(signature_bytes).decode('utf-8')
78
+
79
+ # Append signature
80
+ doc["signature"] = signature_b64
81
+ return doc
82
+ except Exception as e:
83
+ raise CreduEntError(f"Failed during JCS canonicalization or signing: {e}")
84
+
85
+ def main():
86
+ parser = argparse.ArgumentParser(description="Creduent Protocol - agent.json Signing CLI")
87
+ subparsers = parser.add_subparsers(dest="command", help="Subcommand to execute")
88
+
89
+ # generate-keys parser
90
+ subparsers.add_parser("generate-keys", help="Generate Ed25519 private key and public key string")
91
+
92
+ # sign parser
93
+ sign_parser = subparsers.add_parser("sign", help="Sign a draft agent.json payload")
94
+ sign_parser.add_argument("--key", required=True, help="Path to Ed25519 private_key.pem")
95
+ sign_parser.add_argument("--input", required=True, help="Path to unsigned draft JSON file")
96
+ sign_parser.add_argument("--output", required=True, help="Path to write the signed JSON output file")
97
+
98
+ args = parser.parse_args()
99
+
100
+ if args.command == "generate-keys":
101
+ try:
102
+ private_pem, public_key_str = generate_keys()
103
+
104
+ # Save private key PEM locally
105
+ with open("private_key.pem", "w", encoding="utf-8") as f:
106
+ f.write(private_pem)
107
+ print("[SUCCESS] Private key saved to private_key.pem (KEEP THIS SECRET!)")
108
+
109
+ print("\n" + "="*50)
110
+ print("YOUR PUBLIC KEY (Add this to your agent.json):")
111
+ print(public_key_str)
112
+ print("="*50)
113
+ except Exception as e:
114
+ print(f"[-] Error: {e}", file=sys.stderr)
115
+ sys.exit(1)
116
+
117
+ elif args.command == "sign":
118
+ if not os.path.exists(args.key):
119
+ print(f"[-] Error: Key file not found at {args.key}", file=sys.stderr)
120
+ sys.exit(1)
121
+
122
+ if not os.path.exists(args.input):
123
+ print(f"[-] Error: Input document not found at {args.input}", file=sys.stderr)
124
+ sys.exit(1)
125
+
126
+ try:
127
+ with open(args.key, "r", encoding="utf-8") as f:
128
+ private_pem = f.read()
129
+
130
+ with open(args.input, "r", encoding="utf-8") as f:
131
+ draft = json.load(f)
132
+
133
+ signed_doc = sign(draft, private_pem)
134
+
135
+ with open(args.output, "w", encoding="utf-8") as f:
136
+ json.dump(signed_doc, f, indent=2, ensure_ascii=False)
137
+
138
+ print(f"[SUCCESS] Successfully signed and generated {args.output}!")
139
+ except Exception as e:
140
+ print(f"[-] Error: {e}", file=sys.stderr)
141
+ sys.exit(1)
142
+ else:
143
+ parser.print_help()
144
+
145
+ if __name__ == "__main__":
146
+ main()
@@ -0,0 +1,75 @@
1
+ # NOTE: Sync with creduent/utils.py
2
+ import ipaddress
3
+ import socket
4
+ import requests
5
+ from fastapi import HTTPException
6
+ from urllib.parse import urlparse, urljoin
7
+
8
+ def is_private_ip(ip_str: str) -> bool:
9
+ try:
10
+ ip = ipaddress.ip_address(ip_str)
11
+ return ip.is_private or ip.is_loopback or ip.is_link_local
12
+ except ValueError:
13
+ return False
14
+
15
+ def safe_requests_get(url: str, timeout: int = 5, allow_private: bool = False) -> requests.Response:
16
+ """
17
+ Safe version of requests.get that prevents SSRF by blocking access
18
+ to private IP ranges, including redirect targets.
19
+ """
20
+ history = []
21
+ current_url = url
22
+ for _ in range(5): # Follow max 5 redirects
23
+ parsed = urlparse(current_url)
24
+ host = parsed.netloc.split(':')[0]
25
+ try:
26
+ ip = socket.gethostbyname(host)
27
+ if not allow_private and is_private_ip(ip):
28
+ raise HTTPException(status_code=400, detail="Access to private IP ranges is blocked.")
29
+ except socket.gaierror:
30
+ # If DNS resolution fails here, let requests handle the connection error
31
+ pass
32
+
33
+ response = requests.get(current_url, timeout=timeout, allow_redirects=False)
34
+ if response.is_redirect:
35
+ history.append(response)
36
+ next_url = response.headers.get('location')
37
+ if not next_url:
38
+ break
39
+ current_url = urljoin(current_url, next_url)
40
+ else:
41
+ response.history = history
42
+ return response
43
+
44
+ response = requests.get(current_url, timeout=timeout, allow_redirects=False)
45
+ response.history = history
46
+ return response
47
+
48
+ def load_dotenv():
49
+ """
50
+ Manually loads .env.local or .env file from the project base directory
51
+ into os.environ for local testing/development.
52
+ """
53
+ import os
54
+ # Try to find base dir (where .env.local resides)
55
+ base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
56
+ for filename in ['.env.local', '.env']:
57
+ filepath = os.path.join(base_dir, filename)
58
+ if os.path.exists(filepath):
59
+ try:
60
+ with open(filepath, 'r', encoding='utf-8') as f:
61
+ for line in f:
62
+ line = line.strip()
63
+ if not line or line.startswith('#'):
64
+ continue
65
+ if '=' in line:
66
+ key, val = line.split('=', 1)
67
+ key = key.strip()
68
+ val = val.strip()
69
+ # Strip quotes
70
+ if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
71
+ val = val[1:-1]
72
+ # Set and overwrite in environment
73
+ os.environ[key] = val
74
+ except Exception as e:
75
+ print(f"[-] Warning: Failed to load environment file {filename}: {e}")
@@ -0,0 +1,238 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import base64
5
+ import argparse
6
+ from dataclasses import dataclass
7
+ from typing import List, Optional
8
+ from cryptography.hazmat.primitives.asymmetric import ed25519
9
+ from cryptography.exceptions import InvalidSignature
10
+
11
+ from creduent.crypto import canonicalize
12
+ from creduent.utils import safe_requests_get
13
+ from creduent.exceptions import VerificationError
14
+
15
+ @dataclass
16
+ class VerifyResult:
17
+ valid: bool
18
+ agent_id: str
19
+ public_key: str
20
+ endpoint: str
21
+ capabilities: List[str]
22
+ error: Optional[str] = None
23
+
24
+ def resolve_target(target: str) -> str:
25
+ """
26
+ Resolves agent_id, domain, or URL to a fetchable well-known agent.json URL.
27
+ """
28
+ target = target.strip()
29
+
30
+ # 1. Handle HTTP/HTTPS URLs
31
+ if target.startswith("http://") or target.startswith("https://"):
32
+ from urllib.parse import urlparse, urlunparse
33
+ parsed = urlparse(target)
34
+ if not parsed.path.endswith("/.well-known/agent.json"):
35
+ # Append well-known path if not present
36
+ path = parsed.path.rstrip('/') + "/.well-known/agent.json"
37
+ return urlunparse((parsed.scheme, parsed.netloc, path, parsed.params, parsed.query, parsed.fragment))
38
+ return target
39
+
40
+ # 2. Handle agent:// URIs
41
+ if target.startswith("agent://"):
42
+ from urllib.parse import urlparse
43
+ parsed = urlparse(target)
44
+ namespace = parsed.netloc
45
+
46
+ # Default mapping fallback for testing/reconbot
47
+ if target == "agent://creduent/reconbot":
48
+ return "https://api.idevsec.com/.well-known/agent.json"
49
+
50
+ # Try to resolve domain from Creduent registry
51
+ try:
52
+ registry_url = "https://api.idevsec.com/registry/attest/" + target
53
+ response = safe_requests_get(registry_url, timeout=5)
54
+ if response.status_code == 200:
55
+ attestation = response.json()
56
+ domain = attestation.get("domain")
57
+ if domain:
58
+ return f"https://{domain}/.well-known/agent.json"
59
+ except Exception:
60
+ pass
61
+
62
+ # Fallback to default namespace resolution
63
+ return f"https://api.{namespace}.ai/.well-known/agent.json"
64
+
65
+ # 3. Handle domain or host name (e.g. "api.idevsec.com")
66
+ scheme = "http" if "localhost" in target or "127.0.0.1" in target else "https"
67
+ return f"{scheme}://{target}/.well-known/agent.json"
68
+
69
+ def verify(target: str | dict) -> VerifyResult:
70
+ """
71
+ Verify a self-signed agent.json — from URL, agent:// URI, domain string, or dict.
72
+ Returns VerifyResult.
73
+ Raises VerificationError on connection or critical resolution failures.
74
+ """
75
+ doc = None
76
+ if isinstance(target, dict):
77
+ doc = target
78
+ elif isinstance(target, str):
79
+ resolved_url = resolve_target(target)
80
+ try:
81
+ allow_private = "localhost" in resolved_url or "127.0.0.1" in resolved_url
82
+ response = safe_requests_get(resolved_url, timeout=5, allow_private=allow_private)
83
+ if response.status_code != 200:
84
+ raise VerificationError(f"Failed to fetch agent.json: HTTP status {response.status_code}")
85
+ doc = response.json()
86
+ except Exception as e:
87
+ # Re-raise VerificationError or wrap other errors
88
+ if isinstance(e, VerificationError):
89
+ raise
90
+ err_msg = str(e)
91
+ if hasattr(e, 'detail'):
92
+ err_msg = e.detail
93
+ raise VerificationError(f"Failed to retrieve agent.json: {err_msg}")
94
+ else:
95
+ raise VerificationError("Target must be a dictionary or a string")
96
+
97
+ if not isinstance(doc, dict):
98
+ return VerifyResult(
99
+ valid=False,
100
+ agent_id="",
101
+ public_key="",
102
+ endpoint="",
103
+ capabilities=[],
104
+ error="Parsed document is not a JSON object"
105
+ )
106
+
107
+ agent_id = doc.get("agent_id", "")
108
+ public_key_str = doc.get("public_key", "")
109
+ endpoint = doc.get("endpoint", "")
110
+ capabilities = doc.get("capabilities", [])
111
+
112
+ # 1. Structural schema manual validation
113
+ required = ["version", "agent_id", "owner", "public_key", "endpoint", "capabilities", "signature"]
114
+ for field in required:
115
+ if field not in doc:
116
+ return VerifyResult(
117
+ valid=False,
118
+ agent_id=agent_id,
119
+ public_key=public_key_str,
120
+ endpoint=endpoint,
121
+ capabilities=capabilities,
122
+ error=f"Missing required field '{field}' in agent.json"
123
+ )
124
+
125
+ if doc.get("version") != "1.0":
126
+ return VerifyResult(
127
+ valid=False,
128
+ agent_id=agent_id,
129
+ public_key=public_key_str,
130
+ endpoint=endpoint,
131
+ capabilities=capabilities,
132
+ error=f"Unsupported protocol version: {doc.get('version')}"
133
+ )
134
+
135
+ if not isinstance(capabilities, list):
136
+ return VerifyResult(
137
+ valid=False,
138
+ agent_id=agent_id,
139
+ public_key=public_key_str,
140
+ endpoint=endpoint,
141
+ capabilities=[],
142
+ error="Capabilities field must be a list of strings"
143
+ )
144
+
145
+ # 2. Cryptographic signature check
146
+ if not public_key_str.startswith("ed25519:"):
147
+ return VerifyResult(
148
+ valid=False,
149
+ agent_id=agent_id,
150
+ public_key=public_key_str,
151
+ endpoint=endpoint,
152
+ capabilities=capabilities,
153
+ error="Unsupported public key format. Only 'ed25519:' prefix is supported."
154
+ )
155
+
156
+ try:
157
+ pk_b64 = public_key_str.split(":", 1)[1]
158
+ public_key_bytes = base64.b64decode(pk_b64)
159
+ public_key = ed25519.Ed25519PublicKey.from_public_bytes(public_key_bytes)
160
+
161
+ signature_b64 = doc.get("signature")
162
+ signature_bytes = base64.b64decode(signature_b64)
163
+
164
+ # Clone doc and remove signature before canonicalizing
165
+ doc_copy = doc.copy()
166
+ doc_copy.pop("signature", None)
167
+
168
+ canonical_str = canonicalize(doc_copy)
169
+ canonical_bytes = canonical_str.encode('utf-8')
170
+
171
+ public_key.verify(signature_bytes, canonical_bytes)
172
+ except InvalidSignature:
173
+ return VerifyResult(
174
+ valid=False,
175
+ agent_id=agent_id,
176
+ public_key=public_key_str,
177
+ endpoint=endpoint,
178
+ capabilities=capabilities,
179
+ error="Cryptographic signature in agent.json is INVALID."
180
+ )
181
+ except Exception as e:
182
+ return VerifyResult(
183
+ valid=False,
184
+ agent_id=agent_id,
185
+ public_key=public_key_str,
186
+ endpoint=endpoint,
187
+ capabilities=capabilities,
188
+ error=f"Signature verification failed: {e}"
189
+ )
190
+
191
+ return VerifyResult(
192
+ valid=True,
193
+ agent_id=agent_id,
194
+ public_key=public_key_str,
195
+ endpoint=endpoint,
196
+ capabilities=capabilities,
197
+ error=None
198
+ )
199
+
200
+ def main():
201
+ parser = argparse.ArgumentParser(description="Creduent Protocol - agent.json Verification CLI")
202
+ parser.add_argument("target", help="The verification target (domain, URL, agent:// URI, or local JSON path)")
203
+
204
+ args = parser.parse_args()
205
+
206
+ target = args.target
207
+ # Check if target is a local file path
208
+ if os.path.exists(target):
209
+ try:
210
+ with open(target, "r", encoding="utf-8") as f:
211
+ target = json.load(f)
212
+ except Exception as e:
213
+ print(f"[-] Error reading local target file: {e}", file=sys.stderr)
214
+ sys.exit(1)
215
+
216
+ try:
217
+ result = verify(target)
218
+ if result.valid:
219
+ print("\n" + "="*50)
220
+ print("[SUCCESS] IDENTITY VERIFIED & CRYPTOGRAPHICALLY VALID")
221
+ print(f"Agent ID: {result.agent_id}")
222
+ print(f"Public Key: {result.public_key}")
223
+ print(f"Endpoint: {result.endpoint}")
224
+ print(f"Capabilities: {', '.join(result.capabilities)}")
225
+ print("="*50)
226
+ sys.exit(0)
227
+ else:
228
+ print("\n" + "="*50)
229
+ print("[FAILED] VERIFICATION FAILED")
230
+ print(f"Error: {result.error}")
231
+ print("="*50)
232
+ sys.exit(1)
233
+ except Exception as e:
234
+ print(f"[-] Error during verification pipeline: {e}", file=sys.stderr)
235
+ sys.exit(1)
236
+
237
+ if __name__ == "__main__":
238
+ main()
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: creduent
3
+ Version: 0.1.0
4
+ Summary: Creduent Protocol SDK — cryptographic identity for AI agents
5
+ License: MIT
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: cryptography
12
+ Requires-Dist: requests
13
+ Requires-Dist: jcs
14
+ Requires-Dist: fastapi
15
+
16
+ # Creduent Python SDK
17
+
18
+ The official Python SDK for the **Creduent Protocol** — a federated, open trust-verification layer and cryptographic identity infrastructure for autonomous AI agents.
19
+
20
+ ## Installation
21
+
22
+ Install the self-contained package from PyPI with a single command:
23
+
24
+ ```bash
25
+ pip install creduent
26
+ ```
27
+
28
+ ## Quickstart
29
+
30
+ Below is a complete quickstart demonstrating keypair generation, self-signing, verification, registration, and attestation resolution using the SDK.
31
+
32
+ ```python
33
+ from creduent import (
34
+ generate_keys,
35
+ sign,
36
+ verify,
37
+ register,
38
+ attest,
39
+ CreduEntError
40
+ )
41
+
42
+ try:
43
+ # 1. Generate a new Ed25519 keypair
44
+ private_key_pem, public_key_str = generate_keys()
45
+ print(f"Generated Public Key: {public_key_str}\n")
46
+
47
+ # 2. Sign a draft agent.json document
48
+ draft_document = {
49
+ "agent_id": "agent://creduent/reconbot",
50
+ "owner": "Creduent",
51
+ "public_key": public_key_str,
52
+ "endpoint": "https://api.idevsec.com/recon",
53
+ "capabilities": ["osint", "dns_lookup", "vulnerability_scan"]
54
+ }
55
+
56
+ signed_doc = sign(draft_document, private_key_pem)
57
+ print("Signed agent.json:")
58
+ print(signed_doc)
59
+ print()
60
+
61
+ # 3. Verify a self-signed agent.json (from dict, URL, domain, or agent:// URI)
62
+ # Verification with a document dictionary
63
+ result = verify(signed_doc)
64
+ print(f"Self-Signed Verification Result (dict): {result.valid}")
65
+
66
+ # Verification with a live agent URL
67
+ live_result = verify("https://api.idevsec.com/.well-known/agent.json")
68
+ print(f"Live Verification Result (URL): {live_result.valid}")
69
+ print(f"Live Agent ID: {live_result.agent_id}")
70
+ print(f"Live Capabilities: {live_result.capabilities}\n")
71
+
72
+ # 4. Register an agent with the Creduent registry
73
+ # Returns RegisterResult with success (bool), attestation (dict|None), error (str|None)
74
+ reg_result = register(
75
+ agent_id="agent://creduent/reconbot",
76
+ domain="api.idevsec.com",
77
+ agent_json_url="https://api.idevsec.com/.well-known/agent.json"
78
+ )
79
+ print(f"Registration Successful: {reg_result.success}")
80
+ print(f"Attestation: {reg_result.attestation}\n")
81
+
82
+ # 5. Fetch and validate an active attestation for an agent
83
+ # Returns AttestResult with attested (bool), level (str), issued_at, expires_at, error (str|None)
84
+ attest_result = attest("agent://creduent/reconbot")
85
+ print(f"Is Attested: {attest_result.attested}")
86
+ print(f"Attestation Level: {attest_result.level}")
87
+ print(f"Issued At: {attest_result.issued_at}")
88
+ print(f"Expires At: {attest_result.expires_at}\n")
89
+
90
+ except CreduEntError as e:
91
+ print(f"Creduent Protocol Error occurred: {e}")
92
+ ```
93
+
94
+ ## Protocol Specification
95
+
96
+ For full information on the cryptographic standards, JCS canonicalization, and the federated verification workflows, read the complete [Creduent Protocol Specification](https://github.com/cyberfascinate/creduent).
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ creduent/__init__.py
5
+ creduent/attest.py
6
+ creduent/crypto.py
7
+ creduent/exceptions.py
8
+ creduent/register.py
9
+ creduent/sign.py
10
+ creduent/utils.py
11
+ creduent/verify.py
12
+ creduent.egg-info/PKG-INFO
13
+ creduent.egg-info/SOURCES.txt
14
+ creduent.egg-info/dependency_links.txt
15
+ creduent.egg-info/entry_points.txt
16
+ creduent.egg-info/requires.txt
17
+ creduent.egg-info/top_level.txt
18
+ tests/test_sdk.py
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ creduent-sign = creduent.sign:main
3
+ creduent-verify = creduent.verify:main
@@ -0,0 +1,4 @@
1
+ cryptography
2
+ requests
3
+ jcs
4
+ fastapi
@@ -0,0 +1 @@
1
+ creduent
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "creduent"
7
+ version = "0.1.0"
8
+ description = "Creduent Protocol SDK — cryptographic identity for AI agents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: OS Independent",
16
+ ]
17
+ dependencies = [
18
+ "cryptography",
19
+ "requests",
20
+ "jcs",
21
+ "fastapi",
22
+ ]
23
+
24
+ [project.scripts]
25
+ creduent-sign = "creduent.sign:main"
26
+ creduent-verify = "creduent.verify:main"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["."]
30
+ include = ["creduent*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="creduent",
5
+ version="0.1.0",
6
+ packages=find_packages(),
7
+ install_requires=[
8
+ "cryptography",
9
+ "requests",
10
+ "jcs",
11
+ "fastapi",
12
+ ],
13
+ entry_points={
14
+ "console_scripts": [
15
+ "creduent-sign=creduent.sign:main",
16
+ "creduent-verify=creduent.verify:main",
17
+ ]
18
+ }
19
+ )
@@ -0,0 +1,116 @@
1
+ import os
2
+ import sys
3
+ import unittest
4
+ import base64
5
+ from cryptography.hazmat.primitives.asymmetric import ed25519
6
+
7
+ # Add the sdk/ folder to path so we can import creduent directly
8
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
9
+
10
+ from creduent import (
11
+ generate_keys,
12
+ sign,
13
+ verify,
14
+ attest,
15
+ VerificationError,
16
+ AttestationError
17
+ )
18
+
19
+ class TestCreduentSDK(unittest.TestCase):
20
+
21
+ def test_generate_keys(self):
22
+ """1. generate_keys() returns valid PEM + ed25519 prefixed public key"""
23
+ private_key_pem, public_key_str = generate_keys()
24
+
25
+ # Verify private key is PEM format
26
+ self.assertTrue(private_key_pem.startswith("-----BEGIN PRIVATE KEY-----"))
27
+ self.assertTrue(private_key_pem.strip().endswith("-----END PRIVATE KEY-----"))
28
+
29
+ # Verify public key format
30
+ self.assertTrue(public_key_str.startswith("ed25519:"))
31
+ pk_b64 = public_key_str.split(":", 1)[1]
32
+
33
+ # Verify it can be base64 decoded
34
+ pk_bytes = base64.b64decode(pk_b64)
35
+ self.assertEqual(len(pk_bytes), 32)
36
+
37
+ def test_sign_document(self):
38
+ """2. sign() produces a dict with a valid signature field"""
39
+ private_key_pem, public_key_str = generate_keys()
40
+
41
+ draft = {
42
+ "agent_id": "agent://creduent/reconbot",
43
+ "owner": "Creduent",
44
+ "public_key": public_key_str,
45
+ "endpoint": "https://api.idevsec.com/recon",
46
+ "capabilities": ["osint", "dns_lookup", "vulnerability_scan"]
47
+ }
48
+
49
+ signed_doc = sign(draft, private_key_pem)
50
+
51
+ self.assertIn("signature", signed_doc)
52
+ self.assertIn("issued_at", signed_doc)
53
+ self.assertEqual(signed_doc["version"], "1.0")
54
+
55
+ # Verify the signature field is non-empty base64
56
+ sig_b64 = signed_doc["signature"]
57
+ sig_bytes = base64.b64decode(sig_b64)
58
+ self.assertEqual(len(sig_bytes), 64)
59
+
60
+ def test_verify_live_endpoint(self):
61
+ """3. verify() on the live endpoint returns valid=True"""
62
+ target = "https://api.idevsec.com/.well-known/agent.json"
63
+ try:
64
+ result = verify(target)
65
+ self.assertTrue(result.valid)
66
+ self.assertIsNone(result.error)
67
+ self.assertEqual(result.agent_id, "agent://creduent/reconbot")
68
+ except VerificationError as e:
69
+ # If the network or live endpoint is completely unreachable, skip or print warning
70
+ print(f"\n[WARNING] Live verification test skipped/failed due to network: {e}")
71
+
72
+ def test_verify_tampered_dict(self):
73
+ """4. verify() on a tampered dict returns valid=False"""
74
+ private_key_pem, public_key_str = generate_keys()
75
+
76
+ draft = {
77
+ "agent_id": "agent://creduent/reconbot",
78
+ "owner": "Creduent",
79
+ "public_key": public_key_str,
80
+ "endpoint": "https://api.idevsec.com/recon",
81
+ "capabilities": ["osint", "dns_lookup", "vulnerability_scan"]
82
+ }
83
+
84
+ signed_doc = sign(draft, private_key_pem)
85
+
86
+ # 4a. Verify untampered dict is valid
87
+ res_ok = verify(signed_doc)
88
+ self.assertTrue(res_ok.valid)
89
+
90
+ # 4b. Tamper with a field
91
+ tampered_doc = signed_doc.copy()
92
+ tampered_doc["owner"] = "Not Creduent"
93
+
94
+ res_tampered = verify(tampered_doc)
95
+ self.assertFalse(res_tampered.valid)
96
+ self.assertIsNotNone(res_tampered.error)
97
+
98
+ def test_attest_live(self):
99
+ """5. attest() against https://api.idevsec.com returns a valid AttestResult"""
100
+ try:
101
+ result = attest("agent://creduent/reconbot", "https://api.idevsec.com")
102
+ # The agent might or might not be currently registered in the database,
103
+ # but the result should be a valid AttestResult dataclass
104
+ self.assertIn(result.attested, [True, False])
105
+ if result.attested:
106
+ self.assertEqual(result.level, "verified")
107
+ self.assertIsNotNone(result.issued_at)
108
+ self.assertIsNotNone(result.expires_at)
109
+ self.assertIsNone(result.error)
110
+ else:
111
+ self.assertIsNotNone(result.error)
112
+ except AttestationError as e:
113
+ print(f"\n[WARNING] Live attestation test skipped/failed due to network: {e}")
114
+
115
+ if __name__ == '__main__':
116
+ unittest.main()