Skip to content

Invalid DPoP Proof

Type URI: https://arcp.0x001.tech/docs/problems/dpop-invalid
HTTP Status: 401 Unauthorized
Title: Invalid DPoP Proof

Description

This problem occurs when a DPoP proof is provided but fails validation. The proof may have an invalid signature, expired timestamp, incorrect HTTP method/URI binding, or malformed structure.

When This Occurs

  • DPoP proof JWT has invalid signature
  • Proof timestamp is too old or in the future
  • HTTP method (htm) doesn't match the request method
  • HTTP URI (htu) doesn't match the request URI
  • Missing required claims (jti, htm, htu, iat)
  • Malformed JWK in proof header
  • Replay attack detected (jti reused)

Example Response

{
  "type": "https://arcp.0x001.tech/docs/problems/dpop-invalid",
  "title": "Invalid DPoP Proof",
  "status": 401,
  "detail": "DPoP proof signature validation failed",
  "instance": "/agents/validate_compliance",
  "timestamp": "2024-01-15T10:30:00Z",
  "request_id": "req_xyz789"
}

Common Scenarios

1. Signature Verification Failed

# DPoP proof signed with wrong key
curl -X POST "http://localhost:8001/agents/validate_compliance" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IlJTMjU2Ii..." \
  -H "Content-Type: application/json"

2. HTTP Method Mismatch

# DPoP created for POST but used in GET
curl "http://localhost:8001/agents/some-resource" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IlJTMjU2Ii..."

3. Expired Timestamp

# DPoP proof created more than 60 seconds ago
curl -X POST "http://localhost:8001/agents/validate_compliance" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
  -H "DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IlJTMjU2Ii..."

Resolution Steps

1. Verify DPoP Proof Creation

Ensure correct proof generation:

import jwt
import time
import uuid

def create_dpop_proof(private_key, method, uri, access_token=None):
    """Create a valid DPoP proof."""

    # Get public key JWK
    public_key = private_key.public_key()
    jwk = get_jwk_from_public_key(public_key)

    # Create claims
    claims = {
        "jti": str(uuid.uuid4()),
        "htm": method,  # MUST match request method
        "htu": uri,     # MUST match request URI
        "iat": int(time.time())  # Current timestamp
    }

    # Add access token hash if present
    if access_token:
        import hashlib
        import base64
        hash_bytes = hashlib.sha256(access_token.encode()).digest()
        claims["ath"] = base64.urlsafe_b64encode(hash_bytes).decode().rstrip("=")

    # Sign with private key
    proof = jwt.encode(
        claims,
        private_key,
        algorithm="RS256",
        headers={"typ": "dpop+jwt", "jwk": jwk}
    )

    return proof

2. Check HTTP Method and URI

# Correct usage
method = "POST"
uri = "http://localhost:8001/agents/validate_compliance"

dpop_proof = create_dpop_proof(private_key, method, uri, access_token)

response = requests.post(
    uri,  # Same URI
    headers={
        "Authorization": f"Bearer {access_token}",
        "DPoP": dpop_proof
    },
    json=data
)

3. Generate Fresh Proof for Each Request

# DON'T reuse DPoP proofs
# DO create new proof for each request

for request in requests_to_make:
    # Create fresh proof
    dpop_proof = create_dpop_proof(
        private_key,
        request.method,
        request.uri,
        access_token
    )

    # Make request
    response = request.execute(dpop_proof)

4. Verify Key Consistency

# Use the SAME private key that was used to bind the token
# Don't generate new keys for each proof

# Store key securely
private_key = load_private_key("path/to/key.pem")

# Use this key for all proofs
dpop_proof = create_dpop_proof(private_key, method, uri)

Validation Rules

ARCP validates the following:

  1. JWT Structure
  2. Valid JWT format
  3. Header contains typ: "dpop+jwt"
  4. Header contains valid JWK

  5. Signature

  6. Signature verifies with public key from JWK
  7. Algorithm is RS256 or ES256

  8. Claims

  9. jti: Present and unique (not replayed)
  10. htm: Matches request HTTP method
  11. htu: Matches request URI (scheme + host + path)
  12. iat: Within acceptable time window (±60 seconds)
  13. ath: If present, matches hash of access token

  14. Timestamp

  15. Not more than 60 seconds old
  16. Not in the future

Technical Details

DPoP Proof Lifetime

# Default: 60 seconds
DPOP_PROOF_MAX_AGE = 60

# Proof is valid if:
# current_time - 60 <= iat <= current_time

URI Matching

# Full URI comparison (case-sensitive)
# Includes: scheme, host, port, path
# Excludes: query parameters, fragment

Expected: "http://localhost:8001/agents/validate_compliance"
Valid:    "http://localhost:8001/agents/validate_compliance"
Invalid:  "http://localhost:8001/agents/validate_compliance?foo=bar"
Invalid:  "https://localhost:8001/agents/validate_compliance"

Common Mistakes

  1. Using Wrong Key: Proof signed with different key than token binding
  2. Reusing Proofs: Same jti used multiple times
  3. URI Mismatch: Including query parameters or wrong scheme
  4. Time Skew: Server and client clocks out of sync
  5. Method Mismatch: Proof for GET used in POST request

References