Skip to content

Commit 5d8652f

Browse files
authored
Merge pull request #3 from lestrrat-go/http
Major refactor of http code
2 parents 1b91c47 + d9dbf9a commit 5d8652f

File tree

15 files changed

+1353
-1347
lines changed

15 files changed

+1353
-1347
lines changed

README.md

Lines changed: 26 additions & 236 deletions
Original file line numberDiff line numberDiff line change
@@ -12,235 +12,37 @@ go get github.com/lestrrat-go/htmsig
1212

1313
## Quick Start
1414

15-
### Basic Request Signing
15+
### Client/Server Example
1616

17-
<!-- INCLUDE(example_test.go) -->
18-
```go
19-
package htmsig_test
20-
21-
import (
22-
"context"
23-
"crypto/rand"
24-
"crypto/rsa"
25-
"fmt"
26-
"net/http"
27-
28-
"github.com/lestrrat-go/htmsig"
29-
"github.com/lestrrat-go/htmsig/component"
30-
"github.com/lestrrat-go/htmsig/input"
31-
)
32-
33-
// ExampleSign demonstrates how to sign an HTTP request
34-
func ExampleSign() {
35-
// Generate an RSA key pair
36-
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
37-
if err != nil {
38-
panic(err)
39-
}
40-
41-
// Create an HTTP request to sign
42-
req, err := http.NewRequest(http.MethodPost, "https://example.com/api/data", nil)
43-
if err != nil {
44-
panic(err)
45-
}
46-
req.Header.Set("Content-Type", "application/json")
47-
req.Header.Set("Date", "Tue, 20 Apr 2021 02:07:55 GMT")
48-
49-
// Create signature definition
50-
def, err := input.NewDefinitionBuilder().
51-
Label("my-signature").
52-
Components(
53-
component.Method(),
54-
component.TargetURI(),
55-
component.Authority(),
56-
component.New("content-type"),
57-
component.New("date"),
58-
).
59-
KeyID("my-key-id").
60-
Algorithm("rsa-pss-sha512").
61-
Build()
62-
if err != nil {
63-
panic(err)
64-
}
65-
66-
// Create input value containing the signature definition
67-
inputValue := input.NewValueBuilder().AddDefinition(def).MustBuild()
68-
69-
// Sign the request
70-
ctx := component.WithRequestInfoFromHTTP(context.Background(), req)
71-
err = htmsig.SignRequest(ctx, req.Header, inputValue, privateKey)
72-
if err != nil {
73-
panic(err)
74-
}
75-
76-
fmt.Printf("Signed request has Signature-Input: %t\n", req.Header.Get("Signature-Input") != "")
77-
fmt.Printf("Signed request has Signature: %t\n", req.Header.Get("Signature") != "")
78-
// Output:
79-
// Signed request has Signature-Input: true
80-
// Signed request has Signature: true
81-
}
82-
83-
// ExampleVerify demonstrates how to verify an HTTP request signature
84-
func ExampleVerify() {
85-
// Generate an RSA key pair for the example
86-
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
87-
if err != nil {
88-
panic(err)
89-
}
90-
publicKey := &privateKey.PublicKey
91-
92-
// Create and sign a request first
93-
req, err := http.NewRequest("POST", "https://example.com/api/data", nil)
94-
if err != nil {
95-
panic(err)
96-
}
97-
req.Header.Set("Content-Type", "application/json")
98-
req.Header.Set("Date", "Tue, 20 Apr 2021 02:07:55 GMT")
99-
100-
def, err := input.NewDefinitionBuilder().
101-
Label("my-signature").
102-
Components(
103-
component.Method(),
104-
component.TargetURI(),
105-
component.Authority(),
106-
component.New("content-type"),
107-
component.New("date"),
108-
).
109-
KeyID("my-key-id").
110-
Algorithm("rsa-pss-sha512").
111-
Build()
112-
if err != nil {
113-
panic(err)
114-
}
115-
116-
inputValue := input.NewValueBuilder().AddDefinition(def).MustBuild()
117-
ctx := component.WithRequestInfoFromHTTP(context.Background(), req)
118-
err = htmsig.SignRequest(ctx, req.Header, inputValue, privateKey)
119-
if err != nil {
120-
panic(err)
121-
}
122-
123-
// Create a key resolver that can resolve keys by ID
124-
keyResolver := &exampleKeyResolver{
125-
keys: map[string]any{
126-
"my-key-id": publicKey,
127-
},
128-
}
129-
130-
// Verify the request signature
131-
ctx = component.WithRequestInfoFromHTTP(context.Background(), req)
132-
err = htmsig.VerifyRequest(ctx, req.Header, keyResolver)
133-
if err != nil {
134-
fmt.Printf("Verification failed: %v\n", err)
135-
return
136-
}
17+
The easiest way to get started is using the `http` package for automatic signing and verification:
13718

138-
fmt.Println("Signature verification successful")
139-
// Output:
140-
// Signature verification successful
141-
}
142-
143-
// exampleKeyResolver is a simple key resolver for the example
144-
type exampleKeyResolver struct {
145-
keys map[string]any
146-
}
147-
148-
func (r *exampleKeyResolver) ResolveKey(keyID string) (any, error) {
149-
key, exists := r.keys[keyID]
150-
if !exists {
151-
return nil, fmt.Errorf("key %q not found", keyID)
152-
}
153-
return key, nil
154-
}
155-
156-
```
157-
source: [example_test.go](https://github.com/lestrrat-go/htmsig/blob/main/example_test.go)
158-
<!-- END INCLUDE -->
159-
160-
### HTTP Server with Signature Verification
161-
162-
<!-- INCLUDE(http/http_example_test.go) -->
19+
<!-- INCLUDE(examples/client_server_example_test.go) -->
16320
```go
164-
package http_test
21+
package htmsig_test
16522

16623
import (
16724
"fmt"
16825
"net/http"
16926
"net/http/httptest"
27+
"strings"
17028
"time"
17129

17230
"github.com/lestrrat-go/htmsig/component"
17331
htmsighttp "github.com/lestrrat-go/htmsig/http"
17432
)
17533

176-
// ExampleWrap demonstrates how to create an HTTP handler with signature verification and response signing.
177-
func ExampleWrap() {
178-
// Use HMAC key for deterministic signatures
179-
hmacKey := []byte("test-hmac-secret-key")
180-
181-
// Create verifier that skips missing signatures for demo
182-
verifier := htmsighttp.NewVerifier(
183-
&htmsighttp.StaticKeyResolver{Key: hmacKey},
184-
htmsighttp.WithSkipOnMissing(true), // Allow unsigned requests for demo
185-
)
186-
187-
// Create the main application handler
188-
appHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
189-
w.Header().Set("Content-Type", "application/json")
190-
w.Header().Set("Date", "Mon, 01 Jan 2024 00:00:00 GMT") // Fixed date for testing
191-
_, _ = fmt.Fprint(w, `{"message": "Hello, signed world!"}`)
192-
})
193-
194-
// Create fixed clock for deterministic timestamps
195-
fixedTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
196-
clock := htmsighttp.FixedClock(fixedTime)
197-
198-
// Create response signer with specific components
199-
signer := htmsighttp.NewResponseSigner(hmacKey, "server-key-1",
200-
htmsighttp.WithSignerComponents(
201-
component.Status(),
202-
component.New("content-type"),
203-
component.New("date"),
204-
),
205-
htmsighttp.WithSignerSignatureLabel("server-key-1"),
206-
htmsighttp.WithClock(clock))
207-
208-
// Wrap with both verification and signing
209-
wrappedHandler := htmsighttp.Wrap(appHandler,
210-
htmsighttp.WithVerifier(verifier),
211-
htmsighttp.WithSigner(signer))
212-
213-
// Test the wrapped handler
214-
req := httptest.NewRequest("GET", "/api/data", nil)
215-
w := httptest.NewRecorder()
216-
wrappedHandler.ServeHTTP(w, req)
217-
218-
fmt.Printf("Status: %d\n", w.Code)
219-
fmt.Printf("Content-Type: %s\n", w.Header().Get("Content-Type"))
220-
fmt.Printf("Signature: %s\n", w.Header().Get("Signature"))
221-
fmt.Printf("Signature-Input: %s\n", w.Header().Get("Signature-Input"))
222-
fmt.Printf("Body: %s\n", w.Body.String())
223-
224-
// Output:
225-
// Status: 200
226-
// Content-Type: application/json
227-
// Signature: server-key-1=:JohOBV+tyheV58LS0h5rT3TfHynbV6bncnEG0jP5vnE=:
228-
// Signature-Input: server-key-1=("@status" "content-type" "date");created=1704067200;keyid="server-key-1"
229-
// Body: {"message": "Hello, signed world!"}
230-
}
231-
232-
// ExampleNewClient demonstrates how to create an HTTP client that automatically signs requests
233-
// and communicates with a server that both verifies incoming signatures and signs responses.
234-
func ExampleNewClient() {
34+
// Example demonstrates a complete client/server interaction
35+
// with HTTP message signatures using the http package.
36+
func Example() {
23537
// Use HMAC key for deterministic signatures
23638
hmacKey := []byte("shared-hmac-secret")
23739

23840
// Create fixed clock for deterministic timestamps
23941
fixedTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
24042
clock := htmsighttp.FixedClock(fixedTime)
24143

242-
// Create a response signer for outgoing responses
243-
signer := htmsighttp.NewResponseSigner(hmacKey, "server-key",
44+
// Create response signer for outgoing responses
45+
responseSigner := htmsighttp.NewResponseSigner(hmacKey, "server-key",
24446
htmsighttp.WithSignerComponents(
24547
component.Status(),
24648
component.New("content-type"),
@@ -250,18 +52,18 @@ func ExampleNewClient() {
25052
// Create the application handler
25153
appHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25254
w.Header().Set("Content-Type", "application/json")
253-
_, _ = fmt.Fprint(w, `{"status": "success"}`)
55+
_, _ = fmt.Fprint(w, `{"message": "Hello, signed world!"}`)
25456
})
25557

256-
// Wrap with just response signing for now (verification can be added separately)
58+
// Wrap handler with response signing
25759
wrappedHandler := htmsighttp.Wrap(appHandler,
258-
htmsighttp.WithSigner(signer))
60+
htmsighttp.WithSigner(responseSigner))
25961

260-
// Create test server with the wrapped handler
62+
// Create test server
26163
server := httptest.NewServer(wrappedHandler)
26264
defer server.Close()
26365

264-
// Create HTTP client with HMAC signing
66+
// Create HTTP client with automatic request signing
26567
client := htmsighttp.NewClient(
26668
hmacKey,
26769
"client-key",
@@ -273,42 +75,30 @@ func ExampleNewClient() {
27375
htmsighttp.WithClock(clock))
27476

27577
// Make a signed request to the server
276-
resp, err := client.Get(server.URL + "/api/test")
78+
resp, err := client.Post(server.URL+"/api/data", "application/json",
79+
strings.NewReader(`{"data": "test"}`))
27780
if err != nil {
27881
fmt.Printf("Request failed: %v\n", err)
27982
return
28083
}
28184
defer func() { _ = resp.Body.Close() }()
28285

283-
// Only print if something went wrong
284-
if resp.StatusCode != http.StatusOK {
285-
fmt.Printf("Unexpected status: %d\n", resp.StatusCode)
286-
return
287-
}
288-
289-
// Check the actual signature value
290-
signature := resp.Header.Get("Signature")
291-
expectedSignature := "sig=:yK6Vo4nyKwMLjsL9qPmXo87yb4FAuUT7ZFmo9duC8QY=:"
292-
if signature != expectedSignature {
293-
fmt.Printf("Unexpected signature: %s\n", signature)
294-
return
295-
}
296-
297-
// Check the signature input value
298-
signatureInput := resp.Header.Get("Signature-Input")
299-
expectedSignatureInput := "sig=(\"@status\" \"content-type\");created=1704067200;keyid=\"server-key\""
300-
if signatureInput != expectedSignatureInput {
301-
fmt.Printf("Unexpected signature input: %s\n", signatureInput)
302-
return
303-
}
86+
fmt.Printf("Response Status: %d\n", resp.StatusCode)
87+
fmt.Printf("Response has Signature: %t\n", resp.Header.Get("Signature") != "")
88+
fmt.Printf("Response has Signature-Input: %t\n", resp.Header.Get("Signature-Input") != "")
30489

30590
// Output:
91+
// Response Status: 200
92+
// Response has Signature: true
93+
// Response has Signature-Input: true
30694
}
30795

30896
```
309-
source: [http/http_example_test.go](https://github.com/lestrrat-go/htmsig/blob/main/http/http_example_test.go)
97+
source: [client_server_example_test.go](https://github.com/lestrrat-go/htmsig/blob/main/client_server_example_test.go)
31098
<!-- END INCLUDE -->
31199

100+
For more detailed examples showing manual signing and verification using the core `htmsig` package, see [manual_example_test.go](https://github.com/lestrrat-go/htmsig/blob/main/manual_example_test.go).
101+
312102
## Supported Algorithms
313103

314104
| Algorithm | RFC 9421 Name | Description |

component/component.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,19 @@ func (c Identifier) Parameters() []string {
6363
return keys
6464
}
6565

66-
// WithParameter creates a new component with the parameter addedto the component.
66+
// WithParameter creates a new component with the parameter added to the component.
6767
func (c Identifier) WithParameter(key string, value any) Identifier {
68-
c.parameters[key] = value
69-
return c
68+
// Create a new parameter map to avoid modifying the original
69+
newParams := make(map[string]any, len(c.parameters)+1)
70+
for k, v := range c.parameters {
71+
newParams[k] = v
72+
}
73+
newParams[key] = value
74+
75+
return Identifier{
76+
name: c.name,
77+
parameters: newParams,
78+
}
7079
}
7180

7281
// HasParameter checks if the component has a specific parameter

0 commit comments

Comments
 (0)