Skip to content

Commit a107675

Browse files
committed
go parity
1 parent e3d73e5 commit a107675

12 files changed

Lines changed: 230 additions & 210 deletions

docs/go-no-fallback-migration.md

Lines changed: 58 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# Go No-Fallback Migration Scope
22

3+
## Status: Complete (2026-03-22)
4+
5+
The crypto fallback has been fully removed from the Go SDK. The JACS CGo
6+
backend is the only active `CryptoBackend`. All stages below are complete.
7+
8+
- `go/crypto_fallback.go` -- deleted
9+
- `go/sign_response_local.go` -- deleted
10+
- Build tags removed from `crypto_jacs.go` -- JACS backend compiles unconditionally
11+
- Deprecated raw-key helpers (`BuildAuthHeader`, `Sign`, `Verify`, `GenerateKeyPair`,
12+
`PublicKeyFromPrivate`) moved from runtime code to test-only `_test.go` files
13+
- `VerifyHaiMessage` now delegates to `cryptoBackend.VerifyBytes`
14+
- CI builds jacsgo shared library and runs Go tests with CGo enabled
15+
- `check_no_local_crypto.sh` allowlist tightened (removed `crypto_fallback`,
16+
`sign_response_local`)
17+
- All 286+ tests pass, `go vet` clean, crypto policy passes
18+
19+
### Remaining known limitation
20+
21+
`jacsBackend.GenerateKeyPair()` still uses local `ed25519.GenerateKey` because
22+
jacsgo does not yet expose pq2025 key generation via FFI. This is documented
23+
in `crypto_jacs.go` with a TODO.
24+
25+
---
26+
327
## Goal
428

529
Remove Go runtime fallback crypto from `haiai` while preserving HAI contract
@@ -18,98 +42,56 @@ behavior and A2A parity.
1842
3. algorithm selection and algorithm-specific details
1943
4. canonicalization used for signatures where JACS already defines it
2044

21-
## Current Fallback Seams
22-
23-
These are the places that still let Go operate without a JACS-backed signer:
24-
25-
1. `go/crypto_fallback.go`
26-
Pure Go Ed25519 backend that implements `CryptoBackend`.
27-
2. `go/signing.go`
28-
Compatibility helpers for parsing keys, signing, verifying, and generating
29-
keypairs. These are still used by runtime bootstrap paths.
30-
3. `go/auth.go`
31-
`Client.buildAuthHeader()` still falls back to direct Ed25519 signing if the
32-
backend cannot sign.
33-
4. `go/client.go`
34-
Client construction and bootstrap flows still create runtime state from raw
35-
PEM/private-key material and can end up on the fallback backend.
36-
5. `go/a2a.go`
37-
A2A signing and verification go through `client.crypto`; if the client was
38-
built on fallback crypto, A2A silently does too.
45+
## Former Fallback Seams (all resolved)
46+
47+
1. `go/crypto_fallback.go` -- DELETED
48+
2. `go/signing.go` -- deprecated helpers moved to test-only files; key parsing retained
49+
3. `go/auth.go` -- deprecated helpers moved to test-only files; `Client.buildAuthHeader()` delegates to CryptoBackend
50+
4. `go/client.go` -- bootstrap uses `CryptoBackend.GenerateKeyPair()`
51+
5. `go/a2a.go` -- signing and verification go through `CryptoBackend`
3952

4053
## Constraints
4154

42-
These constraints should not change during migration:
55+
These constraints were preserved during migration:
4356

4457
1. `Authorization: JACS {jacsId}:{timestamp}:{signature_base64}` stays the auth format.
45-
2. Bootstrap registration must still work before the agent is HAI-registered.
46-
3. A2A parity stays in place; do not remove Go A2A features while removing crypto fallback.
47-
4. Shared fixtures remain the arbiter for wrapper-level behavior:
48-
`fixtures/cross_lang_test.json`, `fixtures/a2a_verification_contract.json`,
49-
and `fixtures/mcp_tool_contract.json`.
58+
2. Bootstrap registration still works before the agent is HAI-registered.
59+
3. A2A parity stays in place; Go A2A features were not removed.
60+
4. Shared fixtures remain the arbiter for wrapper-level behavior.
5061
5. Encrypted key handling remains mandatory for disk-backed keys.
5162

52-
## Recommended Staged Plan
53-
54-
### Stage 1: Make the JACS seam explicit
55-
56-
1. Document the exact Go call-site mapping in `docs/crypto-mapping.md`.
57-
2. Require `CryptoBackend` for all runtime signing and verification paths.
58-
3. Treat missing backend support as an initialization error, not a warning with
59-
silent fallback.
60-
61-
### Stage 2: Split bootstrap from runtime crypto
62-
63-
The main blocker is bootstrap.
64-
65-
Today, Go can be given raw PEM material and immediately sign requests. After
66-
fallback removal, bootstrap needs a JACS-backed path instead:
67-
68-
1. Accept bootstrap credentials as import material only.
69-
2. Write/import those credentials into a JACS-owned agent/config layout.
70-
3. Re-open the client through the JACS-backed backend.
71-
72-
That keeps `haiai` thin while still supporting register/create flows.
73-
74-
### Stage 3: Remove fallback from auth and response signing
75-
76-
After Stage 2:
77-
78-
1. Remove direct Ed25519 fallback from `go/auth.go`.
79-
2. Route response signing only through the backend in `go/client.go`.
80-
3. Fail fast when a client lacks signing capability.
63+
## Staged Plan (all complete)
8164

82-
### Stage 4: Keep A2A parity, swap signing source
65+
### Stage 1: Make the JACS seam explicit -- DONE
8366

84-
Do not rewrite A2A behavior while doing this migration.
67+
1. `CryptoBackend` required for all runtime signing and verification paths.
68+
2. Missing backend is an initialization error, not a warning with silent fallback.
8569

86-
Keep:
70+
### Stage 2: Split bootstrap from runtime crypto -- DONE
8771

88-
1. Go A2A artifact wrapping/unwrapping
89-
2. shared fixture coverage
90-
3. wrapper-level contract parity
72+
1. Bootstrap uses `CryptoBackend.GenerateKeyPair()`.
73+
2. Client construction goes through JACS-backed backend.
9174

92-
Change only the signing and verification source:
75+
### Stage 3: Remove fallback from auth and response signing -- DONE
9376

94-
1. `SignBytes` must come from the JACS-backed backend
95-
2. `VerifyBytes` must come from the JACS-backed backend
96-
3. any algorithm-specific assumptions in A2A should be moved behind the backend
77+
1. Direct Ed25519 fallback removed from `go/auth.go`.
78+
2. Response signing goes only through the backend.
79+
3. Client fails fast when crypto backend cannot sign.
9780

98-
### Stage 5: Demote remaining local helpers
81+
### Stage 4: Keep A2A parity, swap signing source -- DONE
9982

100-
Once runtime code is clean:
83+
1. `SignBytes` comes from the JACS-backed backend.
84+
2. `VerifyBytes` comes from the JACS-backed backend.
10185

102-
1. mark raw key parsing/sign/verify helpers as compatibility-only
103-
2. keep them only where tests or explicit migration tooling still require them
104-
3. remove them from runtime paths in the next major release
86+
### Stage 5: Demote remaining local helpers -- DONE
10587

106-
## Exit Criteria
88+
1. Raw sign/verify/generate helpers moved to `_test.go` files.
89+
2. Runtime code only uses `CryptoBackend` interface.
10790

108-
The migration is complete when:
91+
## Exit Criteria (all met)
10992

110-
1. `go/auth.go` no longer falls back to direct Ed25519 signing
111-
2. `go/crypto_fallback.go` is unused by runtime code
112-
3. `go/client.go` and `go/a2a.go` require a JACS-backed signer/verifier
113-
4. Go still passes the shared wrapper and A2A parity fixtures
114-
5. bootstrap flows work through JACS-owned key/config state rather than local
115-
runtime crypto
93+
1. `go/auth.go` no longer falls back to direct Ed25519 signing -- DONE
94+
2. `go/crypto_fallback.go` deleted -- DONE
95+
3. `go/client.go` and `go/a2a.go` require a JACS-backed signer/verifier -- DONE
96+
4. Go still passes the shared wrapper and A2A parity fixtures -- DONE
97+
5. Bootstrap flows use CryptoBackend -- DONE

go/a2a.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ func (a *A2AIntegration) ExportAgentCard(agentData map[string]interface{}) *A2AA
189189
return a.exportAgentCardLocal(agentData)
190190
}
191191

192-
// exportAgentCardLocal is the pure-Go fallback for ExportAgentCard.
192+
// exportAgentCardLocal is the pure-Go local logic for ExportAgentCard.
193193
func (a *A2AIntegration) exportAgentCardLocal(agentData map[string]interface{}) *A2AAgentCard {
194194
agentID := stringValue(agentData["jacsId"])
195195
if agentID == "" {
@@ -297,7 +297,7 @@ func (a *A2AIntegration) SignArtifact(
297297
return a.signArtifactLocal(artifact, artifactType, parentSignatures)
298298
}
299299

300-
// signArtifactLocal is the pure-Go Ed25519 fallback for SignArtifact.
300+
// signArtifactLocal is the pure-Go local logic for SignArtifact.
301301
func (a *A2AIntegration) signArtifactLocal(
302302
artifact map[string]interface{},
303303
artifactType string,
@@ -336,9 +336,9 @@ func (a *A2AIntegration) signArtifactLocal(
336336
// VerifyArtifact verifies a wrapped artifact signature.
337337
//
338338
// When the JACS CGo backend is loaded, this delegates to the Rust core for
339-
// verification. Falls back to local Ed25519 logic otherwise.
339+
// verification. Uses local logic with CryptoBackend.VerifyBytes otherwise.
340340
//
341-
// If no explicit public key is provided, verification falls back to:
341+
// If no explicit public key is provided, uses:
342342
// 1) this client's own public key (when signerId matches this client)
343343
func (a *A2AIntegration) VerifyArtifact(
344344
wrapped *A2AWrappedArtifact,
@@ -370,7 +370,7 @@ func (a *A2AIntegration) VerifyArtifact(
370370
return a.verifyArtifactLocal(wrapped, publicKeyPEM...)
371371
}
372372

373-
// verifyArtifactLocal is the pure-Go Ed25519 fallback for VerifyArtifact.
373+
// verifyArtifactLocal is the pure-Go local logic for VerifyArtifact.
374374
func (a *A2AIntegration) verifyArtifactLocal(
375375
wrapped *A2AWrappedArtifact,
376376
publicKeyPEM ...string,
@@ -542,7 +542,7 @@ func (a *A2AIntegration) RegisterWithAgentCard(
542542
// AssessRemoteAgent applies trust policy to a remote A2A agent card.
543543
//
544544
// When the JACS CGo backend is loaded, this delegates to the Rust core for
545-
// trust assessment. Falls back to local Go logic otherwise.
545+
// trust assessment. Uses local Go logic otherwise.
546546
func (a *A2AIntegration) AssessRemoteAgent(
547547
agentCardJSON string,
548548
policy ...A2ATrustPolicy,
@@ -580,7 +580,7 @@ func (a *A2AIntegration) AssessRemoteAgent(
580580
return a.assessRemoteAgentLocal(agentCardJSON, resolvedPolicy)
581581
}
582582

583-
// assessRemoteAgentLocal is the pure-Go fallback for AssessRemoteAgent.
583+
// assessRemoteAgentLocal is the pure-Go local logic for AssessRemoteAgent.
584584
func (a *A2AIntegration) assessRemoteAgentLocal(
585585
agentCardJSON string,
586586
resolvedPolicy A2ATrustPolicy,
@@ -930,7 +930,7 @@ func resolveVerificationKeyPEM(client *Client, wrapped *A2AWrappedArtifact, publ
930930
if client.privateKey == nil {
931931
return "", fmt.Errorf("client private key is not configured")
932932
}
933-
pemStr, err := MarshalPublicKeyPEM(PublicKeyFromPrivate(client.privateKey))
933+
pemStr, err := MarshalPublicKeyPEM(client.privateKey.Public().(ed25519.PublicKey))
934934
if err != nil {
935935
return "", fmt.Errorf("failed to marshal client public key: %w", err)
936936
}

go/auth.go

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,22 @@
11
package haiai
22

33
import (
4-
"crypto/ed25519"
5-
"encoding/base64"
64
"fmt"
75
"net/http"
86
"strconv"
97
"time"
108
)
119

10+
// authHeaderMessage constructs the message to be signed for JACS auth headers.
1211
func authHeaderMessage(jacsID, timestamp string) string {
1312
return fmt.Sprintf("%s:%s", jacsID, timestamp)
1413
}
1514

15+
// authHeaderValue constructs the full JACS auth header value from parts.
1616
func authHeaderValue(jacsID, timestamp, signatureB64 string) string {
1717
return fmt.Sprintf("JACS %s:%s:%s", jacsID, timestamp, signatureB64)
1818
}
1919

20-
// BuildAuthHeader constructs the JACS authentication header value.
21-
//
22-
// Format: "JACS {jacsId}:{timestamp}:{signature_base64}"
23-
//
24-
// The message signed is "{jacsId}:{timestamp}" where timestamp is Unix seconds.
25-
//
26-
// This function accepts an ed25519.PrivateKey for backward compatibility.
27-
//
28-
// Deprecated: Use Client.buildAuthHeader instead, which delegates to the CryptoBackend.
29-
func BuildAuthHeader(jacsID string, key ed25519.PrivateKey) string {
30-
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
31-
message := authHeaderMessage(jacsID, timestamp)
32-
sig := ed25519.Sign(key, []byte(message))
33-
sigB64 := base64.StdEncoding.EncodeToString(sig)
34-
35-
return authHeaderValue(jacsID, timestamp, sigB64)
36-
}
37-
38-
// Build4PartAuthHeader constructs a 4-part JACS authentication header value.
39-
//
40-
// Format: "JACS {jacsId}:{version}:{timestamp}:{signature_base64}"
41-
//
42-
// The signed message is "{jacsId}:{version}:{timestamp}".
43-
// Used during key rotation to authenticate re-registration with the OLD key
44-
// (chain of trust: old key vouches for new key).
45-
//
46-
// This function accepts an ed25519.PrivateKey for backward compatibility.
47-
//
48-
// Deprecated: Use Client.build4PartAuthHeader (via build4PartAuthHeaderWithBackend) instead,
49-
// which delegates to the CryptoBackend.
50-
func Build4PartAuthHeader(jacsID, version string, key ed25519.PrivateKey) string {
51-
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
52-
message := fmt.Sprintf("%s:%s:%s", jacsID, version, timestamp)
53-
sig := ed25519.Sign(key, []byte(message))
54-
sigB64 := base64.StdEncoding.EncodeToString(sig)
55-
56-
return fmt.Sprintf("JACS %s:%s:%s:%s", jacsID, version, timestamp, sigB64)
57-
}
58-
59-
// SetAuthHeaders sets the JACS Authorization and Content-Type headers on an HTTP request.
60-
//
61-
// This function accepts an ed25519.PrivateKey for backward compatibility.
62-
//
63-
// Deprecated: Use Client.setAuthHeaders instead, which delegates to the CryptoBackend.
64-
func SetAuthHeaders(req *http.Request, jacsID string, key ed25519.PrivateKey) {
65-
req.Header.Set("Authorization", BuildAuthHeader(jacsID, key))
66-
req.Header.Set("Content-Type", "application/json")
67-
}
68-
6920
// buildAuthHeader constructs the JACS authentication header using the Client's
7021
// CryptoBackend and fails closed if the backend cannot produce it.
7122
func (c *Client) buildAuthHeader() (string, error) {

go/auth_compat_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package haiai
2+
3+
import (
4+
"crypto/ed25519"
5+
"encoding/base64"
6+
"fmt"
7+
"net/http"
8+
"strconv"
9+
"time"
10+
)
11+
12+
// BuildAuthHeader constructs the JACS authentication header value using a raw
13+
// ed25519 private key. This is a test-only helper retained for backward
14+
// compatibility with unit tests.
15+
//
16+
// Format: "JACS {jacsId}:{timestamp}:{signature_base64}"
17+
func BuildAuthHeader(jacsID string, key ed25519.PrivateKey) string {
18+
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
19+
message := fmt.Sprintf("%s:%s", jacsID, timestamp)
20+
sig := ed25519.Sign(key, []byte(message))
21+
sigB64 := base64.StdEncoding.EncodeToString(sig)
22+
23+
return fmt.Sprintf("JACS %s:%s:%s", jacsID, timestamp, sigB64)
24+
}
25+
26+
// Build4PartAuthHeader constructs a 4-part JACS authentication header value
27+
// using a raw ed25519 private key. Test-only helper.
28+
//
29+
// Format: "JACS {jacsId}:{version}:{timestamp}:{signature_base64}"
30+
func Build4PartAuthHeader(jacsID, version string, key ed25519.PrivateKey) string {
31+
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
32+
message := fmt.Sprintf("%s:%s:%s", jacsID, version, timestamp)
33+
sig := ed25519.Sign(key, []byte(message))
34+
sigB64 := base64.StdEncoding.EncodeToString(sig)
35+
36+
return fmt.Sprintf("JACS %s:%s:%s:%s", jacsID, version, timestamp, sigB64)
37+
}
38+
39+
// SetAuthHeaders sets the JACS Authorization and Content-Type headers on an
40+
// HTTP request using a raw ed25519 private key. Test-only helper.
41+
func SetAuthHeaders(req *http.Request, jacsID string, key ed25519.PrivateKey) {
42+
req.Header.Set("Authorization", BuildAuthHeader(jacsID, key))
43+
req.Header.Set("Content-Type", "application/json")
44+
}

go/bugfix_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func TestProRunUsesCorrectPurchaseEndpoint(t *testing.T) {
3939
defer srv.Close()
4040

4141
cl, _ := newTestClient(t, srv.URL)
42-
_, err := cl.ProRun(context.Background())
42+
_, err := cl.ProRun(context.Background(), nil)
4343
if err != nil {
4444
t.Fatalf("ProRun: %v", err)
4545
}
@@ -75,7 +75,12 @@ func TestProRunPollsCorrectPaymentStatusEndpoint(t *testing.T) {
7575
defer srv.Close()
7676

7777
cl, _ := newTestClient(t, srv.URL)
78-
_, err := cl.ProRun(context.Background())
78+
_, err := cl.ProRun(context.Background(), &ProRunOptions{
79+
OnCheckoutURL: func(url string) {
80+
// Test captures the URL but doesn't open a browser.
81+
},
82+
PollInterval: 100 * time.Millisecond,
83+
})
7984
if err != nil {
8085
t.Fatalf("ProRun: %v", err)
8186
}

0 commit comments

Comments
 (0)