Signed Requests (JWS/VC)
Fluree supports cryptographically signed requests using JSON Web Signatures (JWS) and Verifiable Credentials (VC). This provides tamper-proof authentication and enables trustless data exchange.
Note: Requires the credential feature flag. See Compatibility and Feature Flags.
Why Sign Requests?
Signed requests provide:
- Authentication: Prove the identity of the request sender
- Integrity: Ensure the request hasn't been tampered with
- Non-repudiation: Sender cannot deny sending the request
- Authorization: Cryptographically link requests to specific identities
- Auditability: Complete audit trail of who did what
JSON Web Signatures (JWS)
JWS is an IETF standard (RFC 7515) for representing digitally signed content as JSON.
JWS Structure
A JWS consists of three parts:
- Protected Header: Metadata about the signature (base64url-encoded)
- Payload: The actual content being signed (base64url-encoded)
- Signature: Cryptographic signature (base64url-encoded)
Compact Serialization:
eyJhbGciOiJFZDI1NTE5In0.eyJmcm9tIjoibXlkYjptYWluIn0.c2lnbmF0dXJl
|_______header_______|.|______payload______|.|_signature_|
JSON Serialization:
{
"payload": "eyJmcm9tIjoibXlkYjptYWluIn0",
"signatures": [
{
"protected": "eyJhbGciOiJFZDI1NTE5In0",
"signature": "c2lnbmF0dXJl"
}
]
}
Supported Algorithm
Fluree uses EdDSA (Ed25519) for JWS verification. All signed requests must use "alg": "EdDSA" in the protected header.
Creating Signed Requests
Step 1: Prepare the Payload
Create your query or transaction as usual:
{
"@context": {
"ex": "http://example.org/ns/"
},
"from": "mydb:main",
"select": ["?name"],
"where": [
{ "@id": "?person", "ex:name": "?name" }
]
}
Step 2: Encode the Payload
Base64url-encode the JSON payload:
const payload = JSON.stringify(query);
const encodedPayload = base64url.encode(payload);
Step 3: Create the Protected Header
Create a header specifying the algorithm and key ID:
{
"alg": "EdDSA",
"kid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}
Base64url-encode the header:
const header = JSON.stringify({ alg: "EdDSA", kid: keyId });
const encodedHeader = base64url.encode(header);
Step 4: Sign
Create the signing input and sign it:
const signingInput = encodedHeader + "." + encodedPayload;
const signature = sign(signingInput, privateKey);
const encodedSignature = base64url.encode(signature);
Step 5: Construct the JWS
Create the complete JWS:
Compact Format:
const jws = encodedHeader + "." + encodedPayload + "." + encodedSignature;
JSON Format:
{
"payload": "<encodedPayload>",
"signatures": [
{
"protected": "<encodedHeader>",
"signature": "<encodedSignature>"
}
]
}
Step 6: Send the Request
Send the JWS to Fluree:
curl -X POST http://localhost:8090/v1/fluree/query \
-H "Content-Type: application/jose" \
-d '{
"payload": "eyJmcm9tIjoibXlkYjptYWluIn0...",
"signatures": [{
"protected": "eyJhbGciOiJFZDI1NTE5In0...",
"signature": "c2lnbmF0dXJl..."
}]
}'
Verifiable Credentials (VC)
Verifiable Credentials are a W3C standard for cryptographically verifiable digital credentials.
VC Structure
A Verifiable Credential includes:
{
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"type": ["VerifiableCredential"],
"issuer": "did:key:z6Mkh...",
"issuanceDate": "2024-01-22T10:00:00Z",
"credentialSubject": {
"id": "did:key:z6Mkh...",
"flureeAction": {
"query": {
"from": "mydb:main",
"select": ["?name"],
"where": [...]
}
}
},
"proof": {
"type": "Ed25519Signature2020",
"created": "2024-01-22T10:00:00Z",
"verificationMethod": "did:key:z6Mkh...#z6Mkh...",
"proofPurpose": "authentication",
"proofValue": "z58DAdFfa9SkqZMVP..."
}
}
Creating a Verifiable Credential
Use a VC library to create signed credentials:
import { issue } from '@digitalbazaar/vc';
const credential = {
'@context': ['https://www.w3.org/2018/credentials/v1'],
type: ['VerifiableCredential'],
issuer: didKey,
issuanceDate: new Date().toISOString(),
credentialSubject: {
id: didKey,
flureeAction: {
query: queryObject
}
}
};
const verifiableCredential = await issue({
credential,
suite: new Ed25519Signature2020({ key: keyPair }),
documentLoader
});
Sending a VC
Send the VC to Fluree:
curl -X POST http://localhost:8090/v1/fluree/query \
-H "Content-Type: application/vc+ld+json" \
-d '{
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiableCredential"],
...
}'
Decentralized Identifiers (DIDs)
Fluree uses DIDs to identify public keys.
Supported DID Methods
did:key - Public key embedded in the DID (recommended):
did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK
did:web - Web-based DID resolution:
did:web:example.com:users:alice
did:ion - ION network DIDs (future support):
did:ion:EiClkZMDxPKqC9c-umQfTkR8vvZ9JPhl_xLDI9Nfk38w5w
DID Resolution
Fluree resolves DIDs to public keys:
- did:key: Public key extracted directly from DID
- did:web: Fetched from
https://example.com/.well-known/did.json - did:ion: Resolved via ION network
Public Key Resolution
Standalone server signed requests verify Ed25519 JWS material from the request
itself (for example embedded JWK / did:key) or configured OIDC/JWKS issuers.
There is no /admin/keys registration endpoint.
Request Verification
Verification Process
When Fluree receives a signed request:
- Extract the signature and header
- Resolve the key ID (kid) to a public key
- Verify the signature using the public key
- Check expiration (if
expclaim present) - Validate issuer (if required)
- Apply authorization policies based on DID
Verification Failure
If verification fails:
Status Code: 401 Unauthorized
Response:
{
"error": "Invalid signature",
"status": 401,
"@type": "err:auth/InvalidSignature"
}
Key Management
Generating Keys
Ed25519 (EdDSA):
import { generateKeyPair } from '@stablelib/ed25519';
const keyPair = generateKeyPair();
// keyPair.publicKey - 32 bytes
// keyPair.secretKey - 64 bytes
Storing Keys
Secure Storage:
- Hardware Security Modules (HSM)
- Key Management Services (AWS KMS, Azure Key Vault)
- Encrypted files with strong passphrases
- Hardware wallets for blockchain-based DIDs
Never:
- Store private keys in code
- Commit keys to version control
- Send keys over insecure channels
- Share keys between applications
Key Rotation
Rotate keys regularly:
- Generate new key pair
- Register new public key with Fluree
- Update client to use new key
- Revoke old key after transition period
- Remove old key from Fluree
Authorization with Signed Requests
Identity-Based Policies
Fluree policies can use the signer's DID for authorization:
{
"@context": {
"ex": "http://example.org/ns/",
"f": "https://ns.flur.ee/db#"
},
"@id": "ex:admin-policy",
"f:policy": [
{
"f:subject": "did:key:z6Mkh...",
"f:action": ["query", "transact"],
"f:allow": true
}
]
}
Role-Based Access
Link DIDs to roles:
{
"@id": "did:key:z6Mkh...",
"@type": "ex:User",
"ex:role": "ex:Administrator"
}
Policy checks the role:
{
"f:policy": [
{
"f:subject": { "ex:role": "ex:Administrator" },
"f:action": "*",
"f:allow": true
}
]
}
Code Examples
JavaScript/TypeScript
import jose from 'jose';
async function signQuery(query: object, privateKey: Uint8Array) {
const payload = JSON.stringify(query);
const jws = await new jose.SignJWT(query)
.setProtectedHeader({ alg: 'EdDSA', kid: 'did:key:z6Mkh...' })
.setIssuedAt()
.setExpirationTime('5m')
.sign(privateKey);
return jws;
}
// Send signed request
const signedQuery = await signQuery(query, privateKey);
const response = await fetch('http://localhost:8090/v1/fluree/query', {
method: 'POST',
headers: { 'Content-Type': 'application/jose' },
body: signedQuery
});
Python
from jwcrypto import jwk, jws
import json
def sign_query(query, private_key):
# Create JWK from private key
key = jwk.JWK.from_json(private_key)
# Create JWS
payload = json.dumps(query).encode('utf-8')
jws_token = jws.JWS(payload)
jws_token.add_signature(key, alg='EdDSA',
protected=json.dumps({"kid": "did:key:z6Mkh..."}))
return jws_token.serialize()
# Send signed request
signed_query = sign_query(query, private_key)
response = requests.post('http://localhost:8090/v1/fluree/query',
headers={'Content-Type': 'application/jose'},
data=signed_query)
Best Practices
1. Use EdDSA (Ed25519)
EdDSA provides:
- Excellent security (128-bit security level)
- Fast signing and verification
- Small signatures (64 bytes)
- Deterministic (no random number generation needed)
2. Include Expiration
Always set an expiration time:
{
"alg": "EdDSA",
"exp": 1642857600
}
3. Use Short Expiration Times
For interactive requests: 5-15 minutes For batch processes: 1-24 hours Never: No expiration
4. Rotate Keys Regularly
Rotate signing keys every 90-180 days.
5. Secure Key Storage
Use proper key management:
- Development: Encrypted local storage
- Production: HSM or KMS
6. Validate on Server
Never trust client-side validation alone. Fluree always validates signatures server-side.
7. Use HTTPS
Always use HTTPS with signed requests to prevent replay attacks.
8. Implement Nonce/JTI
Include a unique identifier to prevent replay:
{
"alg": "EdDSA",
"jti": "unique-request-id-12345"
}
Troubleshooting
"Invalid Signature" Error
Causes:
- Wrong private key used
- Payload modified after signing
- Incorrect base64url encoding
- Algorithm mismatch
Solution: Verify the signing process end-to-end.
"Key Not Found" Error
Causes:
- DID not registered with Fluree
- Incorrect key ID (kid) in header
- DID resolution failed
Solution: Register public key or check DID format.
"Signature Expired" Error
Causes:
- Request sent after expiration time
- Clock skew between client and server
Solution: Use NTP to sync clocks, increase expiration time.
Related Documentation
- Overview - API overview
- Endpoints - API endpoints
- Headers - HTTP headers
- Security - Policy and access control
- Verifiable Data - Verifiable credentials concepts