digital-signature-python-sdk 1.0.0
Python SDK to generate and validate HTTP message signatures per RFC 9421 and RFC 9530
License: Apache-2.0
Requires Python: >=3.9
Keywords: eBay, Digital Signature, ED25519, RSA, HTTP Signatures, RFC 9421, RFC 9530

# eBay Digital Signature SDK for Python

[![PyPI version](https://img.shields.io/pypi/v/digital-signature-python-sdk.svg)](https://pypi.org/project/digital-signature-python-sdk/)
[![Python versions](https://img.shields.io/pypi/pyversions/digital-signature-python-sdk.svg)](https://pypi.org/project/digital-signature-python-sdk/)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

HTTP message signatures provide a mechanism for end-to-end authenticity and integrity for components of an HTTP message.

This Python SDK is designed to simplify the process of generating digital signature headers and also provides a method to validate the digital signature headers.

## Table of contents

* [Digital Signatures for Public API Calls](#digital-signatures-for-public-api-calls)
* [Features](#features)
* [Installation](#installation)
* [Getting Started](#getting-started)
  * [Quick Setup & Run Example Server](#quick-setup--run-example-server)
  * [Testing the Server](#testing-the-server)
  * [Available Endpoints](#available-endpoints)
  * [Troubleshooting](#troubleshooting)
* [Usage](#usage)
* [Configuration](#configuration)
* [Key Generation](#key-generation)
* [Logging](#logging)
* [License](#license)

## Digital Signatures for Public API Calls

Due to regulatory requirements emanating from SCA for our European/UK sellers, we are requiring our developers to add a digital signature for every HTTP call that is made on behalf of a EU/UK seller to certain APIs.

This SDK is generic and the signature scheme is compliant with these finalized IETF standards:

* [RFC 9421: HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421.html)
* [RFC 9530: Digest Fields](https://www.rfc-editor.org/rfc/rfc9530.html)

## Features

This SDK is intended to generate required message signature headers, as per the above IETF standards, and also provides a way to verify signature headers. There is also an example Python FastAPI service included with the SDK.

This SDK incorporates:

* Generation of the following HTTP message signature headers:
  * **Content-Digest**: This header includes a SHA-256 or SHA-512 digest over the HTTP payload (as specified in [RFC 9530 Digest Fields](https://www.rfc-editor.org/rfc/rfc9530.html)), if any. It is not required to be sent for APIs that do not include a request payload (e.g. GET requests).
  * **Signature-Input**: This header indicates which headers and pseudo-headers are included, as well as the order in which they are used when calculating the signature. It is created as specified in [RFC 9421 HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421.html).
  * **Signature**: The value of the Signature header is created as described in [Section 3.1, Creating a Signature](https://www.rfc-editor.org/rfc/rfc9421.html#name-creating-a-signature), of RFC 9421. It uses the Private Key value generated by the [Key Management API](https://developer.ebay.com/api-docs/developer/key-management/overview.html).
  * **x-ebay-signature-key**: This header includes the JWE that is created using the [Key Management API](https://developer.ebay.com/api-docs/developer/key-management/overview.html).
* `sign_message` method to sign the incoming request object
* `validate_signed_message` method to validate the signature of the incoming request object
* There are individual methods as well to generate and validate the headers:
  * `generate_content_digest`
  * `generate_signature`
  * `build_signature_input`
  * `build_signature_base`
  * `validate_content_digest`
  * `validate_signature`

For more details on Digital Signatures for eBay APIs, please refer to the [documentation](https://developer.ebay.com/develop/guides/digital-signatures-for-apis).

## Usage

### Prerequisites

* Python: 3.9 or higher
* pip: Latest version recommended

### Installation

```bash
pip install digital-signature-python-sdk
```

### Quick Start

```python
from digital_signature_sdk import sign_message, RequestToSign

# Load your private key
with open('path/to/privatekey.pem', 'rb') as f:
    private_key = f.read()

# Create a request to sign
request = RequestToSign(
    method="POST",
    path="/v1/sell/finances/transaction",
    authority="api.ebay.com",
    body=b'{"hello": "world"}',
    jwe="<JWE from Key Management API>"
)

# Generate signature headers
headers = sign_message(
    request,
    private_key,
    digest_algo="sha-256",
    algorithm="RSA"  # or "ED25519"
)

# Add headers to your HTTP request
print(headers)
# {
#     'Content-Digest': 'sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:',
#     'x-ebay-signature-key': '<JWE>',
#     'Signature-Input': 'sig1=("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1672531200',
#     'Signature': 'sig1=:MEUCIQDZm...aGFz:'
# }
```

## Getting Started

### Quick Setup & Run Example Server

The fastest way to get started with the SDK is to run the included example server:

```bash
# 1. Clone the repository
git clone https://github.com/eBay/digital-signature-python-sdk.git
cd digital-signature-python-sdk

# 2. Create virtual environment (optional but recommended)
python3 -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# 3. Install dependencies
pip install -e ".[dev,examples]"

# 4. Generate development keys (required - no keys included for security)
python scripts/setup_dev_keys.py

# 5. Set up environment variables
cp .env.example .env
# Edit .env file and set your EBAY_JWE_TOKEN

# 6. Start the example server
python examples/server.py
```

The server will start at `http://localhost:8080`

### Testing the Server

Once the server is running, test it with these curl commands:

#### Generate Signature Headers
```bash
curl -X POST http://localhost:8080/sign \
  -H "Content-Type: application/json" \
  -d '{"orderId": "12345", "amount": 99.99}' \
  -s | jq .
```

**Example Response:**
```json
{
  "content-digest": "sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:",
  "x-ebay-signature-key": "your-jwe-token",
  "signature-input": "sig1=(\"content-digest\" \"x-ebay-signature-key\" \"@method\" \"@path\" \"@authority\");created=1767602259",
  "signature": "sig1=:MEUCIQDZm...aGFz:"
}
```

#### Sign Complete Request
```bash
curl -X POST http://localhost:8080/sign-request \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello eBay API"}' \
  -i
```

#### Validate Signatures

**With jq (recommended):**
```bash
# First generate a signature
RESPONSE=$(curl -s -X POST http://localhost:8080/sign \
  -H "Content-Type: application/json" \
  -d '{"test": "validation"}')

# Then validate it (requires jq)
echo $RESPONSE | jq -r '{
  "method": "POST",
  "path": "/ws/api.dll",
  "authority": "api.sandbox.ebay.com",
  "body": "{\"test\": \"validation\"}",
  "headers": .
}' | curl -X POST http://localhost:8080/validate \
  -H "Content-Type: application/json" \
  -d @- \
  -w "Status: %{http_code}\n"
```

**Without jq (manual process):**
```bash
# 1. Generate signature and copy the output
curl -X POST http://localhost:8080/sign \
  -H "Content-Type: application/json" \
  -d '{"test": "validation"}'

# 2. Use the signature components in validation (replace with actual values)
curl -X POST http://localhost:8080/validate \
  -H "Content-Type: application/json" \
  -d '{
    "method": "POST",
    "path": "/ws/api.dll",
    "authority": "api.sandbox.ebay.com",
    "body": "{\"test\": \"validation\"}",
    "headers": {
      "content-digest": "PASTE_CONTENT_DIGEST_HERE",
      "x-ebay-signature-key": "PASTE_JWE_TOKEN_HERE",
      "signature-input": "PASTE_SIGNATURE_INPUT_HERE",
      "signature": "PASTE_SIGNATURE_HERE"
    }
  }' \
  -w "Status: %{http_code}\n"
```

**Success Response:** HTTP 200 (empty body)
**Failure Response:** HTTP 400 with error message

### Available Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/sign` | POST | Generate signature headers for request body |
| `/sign-request` | POST | Sign complete HTTP request using high-level API |
| `/validate` | POST | Validate signature using individual validation methods |
| `/validate-request` | POST | Validate signature using high-level validation API |

### Troubleshooting

**"ModuleNotFoundError: No module named 'fastapi'"**
```bash
pip install -e ".[dev,examples]"
```

**"FileNotFoundError: Private and public keys must be provided"**
```bash
python scripts/setup_dev_keys.py
```

**"zsh: command not found: python"**
```bash
# Activate virtual environment first
source .venv/bin/activate
# Or use python3
python3 examples/server.py
```

**"zsh: command not found: jq"**
```bash
# Install jq for JSON processing (optional)
brew install jq  # macOS
sudo apt-get install jq  # Ubuntu
```

### Signature Validation

```python
from digital_signature_sdk import validate_signed_message, RequestToSign

# Load public key
with open('path/to/publickey.pem', 'rb') as f:
    public_key = f.read()

# Recreate the request
request = RequestToSign(
    method="POST",
    path="/v1/sell/finances/transaction",
    authority="api.ebay.com",
    body=b'{"hello": "world"}'
)

# Validate
is_valid = validate_signed_message(
    request,
    headers,  # Headers dict with Signature, Signature-Input, Content-Digest
    public_key,
    algorithm="RSA"
)

print(f"Signature valid: {is_valid}")
```

### Configuration

#### Environment Variables (Recommended for Security)

For production applications, use environment variables to keep secrets secure. Copy the `.env.example` file to `.env` and fill in your values:

```bash
cp .env.example .env
```

Edit the `.env` file with your actual values:

```bash
# Your JWE token from eBay's Key Management API
EBAY_JWE_TOKEN=your_actual_jwe_token_here

# Your key file paths
EBAY_PRIVATE_KEY_PATH=path/to/your/privatekey.pem
EBAY_PUBLIC_KEY_PATH=path/to/your/publickey.pem

# API endpoint configuration
EBAY_SIGNATURE_AUTHORITY=api.ebay.com
EBAY_SIGNATURE_PATH=/v1/sell/finances/transaction
```

The SDK automatically loads environment variables with the `EBAY_` prefix and prioritizes them over configuration files.

#### Using Configuration with Environment Variables

```python
from digital_signature_sdk import Config, sign_message, RequestToSign

# Load configuration that prioritizes environment variables
config = Config.load_secure_config("path/to/config.json")

# Load keys using the secure configuration
private_key = config.load_key_file(config.get("privateKey"))

# Get JWE token from environment (falls back to config file)
jwe_token = config.get("jwe")

# Create request
request = RequestToSign(
    method="POST",
    path=config.get("signatureComponents.path"),
    authority=config.get("signatureComponents.authority"),
    body=b'{"hello": "world"}',
    jwe=jwe_token
)

# Sign the request
headers = sign_message(request, private_key)
```

#### JSON Configuration Files (Legacy)

For backward compatibility, you can still use JSON configuration files. However, **avoid storing secrets in these files** for security reasons.

Example configuration for signing-only ([example-config.json](./examples/example-config.json)):

```json
{
  "digestAlgorithm": "sha-256",
  "_jwe_comment": "JWE token should be provided via EBAY_JWE_TOKEN environment variable",
  "privateKey": "examples/keys/ed25519/privatekey.pem",
  "signatureComponents": {
    "method": "POST",
    "authority": "api.ebay.com",
    "path": "/v1/sell/finances/transaction"
  },
  "signatureParams": [
    "content-digest",
    "x-ebay-signature-key",
    "@method",
    "@path",
    "@authority"
  ]
}
```

For signing and validation ([example-config-full.json](examples/example-config-full.json)):

```json
{
  "digestAlgorithm": "sha-256",
  "algorithm": "Ed25519",
  "privateKey": "examples/keys/ed25519/privatekey.pem",
  "publicKey": "examples/keys/ed25519/publickey.pem",
  "jwtPayload": {
    "_pkey_comment": "Public key should be provided via EBAY_JWT_PAYLOAD_PKEY environment variable"
  },
  "signatureComponents": {
    "method": "POST",
    "authority": "api.ebay.com",
    "path": "/v1/sell/finances/transaction"
  },
  "signatureParams": [
    "content-digest",
    "x-ebay-signature-key",
    "@method",
    "@path",
    "@authority"
  ]
}
```

#### Environment Variable Reference

| Environment Variable | Description | Example |
|---------------------|-------------|---------|
| `EBAY_JWE_TOKEN` | JWE token from Key Management API | `<your-jwe-token>` |
| `EBAY_PRIVATE_KEY_PATH` | Path to private key file | `keys/ed25519/privatekey.pem` |
| `EBAY_PUBLIC_KEY_PATH` | Path to public key file | `keys/ed25519/publickey.pem` |
| `EBAY_SIGNATURE_ALGORITHM` | Signature algorithm | `Ed25519` or `RSA` |
| `EBAY_DIGEST_ALGORITHM` | Digest algorithm | `sha-256` or `sha-512` |
| `EBAY_SIGNATURE_AUTHORITY` | API hostname | `api.ebay.com` |
| `EBAY_SIGNATURE_PATH` | API path | `/v1/sell/finances/transaction` |
| `EBAY_SIGNATURE_PARAMS` | Signature parameters (comma-separated) | `content-digest,x-ebay-signature-key,@method` |

### Running the Example

The SDK includes an example FastAPI server. To run it:

```bash
# 1. Generate development keys (first time only)
python scripts/setup_dev_keys.py

# 2. Set up environment variables
cp .env.example .env
# Edit .env with your actual EBAY_JWE_TOKEN

# 3. Install example dependencies
pip install fastapi uvicorn authlib

# 4. Run the example server
python -m examples.server
```

### Key Generation

For security, no cryptographic keys are included in the repository. Generate your own:

```bash
# Quick setup - generates all development keys
python scripts/setup_dev_keys.py

# Advanced options
python -m digital_signature_sdk.key_generator --help
```

### Note for Production Deployment

```
For Production, please host with HTTPS enabled.
```

## Logging

Uses standard Python logging. Configure as needed in your application:

```python
import logging
logging.basicConfig(level=logging.DEBUG)
```

## License

Copyright 2025 eBay Inc.
Developer: Qingyuan Liu

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

<https://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.