mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-29 03:24:07 +00:00
187 lines
4.7 KiB
Go
187 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/therootcompany/golib/auth/jwt"
|
|
)
|
|
|
|
// TestNewAccountJWS verifies ACME newAccount signing: jwk in header,
|
|
// kid absent, typ absent, payload is newAccount body.
|
|
func TestNewAccountJWS(t *testing.T) {
|
|
pk, err := jwt.NewPrivateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signer, err := jwt.NewSigner([]*jwt.PrivateKey{pk})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pubKey, err := pk.PublicKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
jwkBytes, err := json.Marshal(pubKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
payload := NewAccountPayload{
|
|
TermsOfServiceAgreed: true,
|
|
Contact: []string{"mailto:cert-admin@example.com"},
|
|
}
|
|
payloadJSON, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
hdr := &AcmeHeader{
|
|
URL: "https://acme.example.com/acme/new-account",
|
|
Nonce: "abc123-server-nonce",
|
|
JWK: json.RawMessage(jwkBytes),
|
|
// KID is empty for newAccount -- jwk is used instead.
|
|
}
|
|
|
|
raw, err := signer.SignRaw(hdr, payloadJSON)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify protected header has ACME fields and no typ.
|
|
headerJSON, err := base64.RawURLEncoding.DecodeString(string(raw.Protected))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(headerJSON, &decoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, ok := decoded["typ"]; ok {
|
|
t.Error("ACME header must not contain typ")
|
|
}
|
|
if decoded["alg"] != "EdDSA" {
|
|
t.Errorf("alg = %v, want EdDSA", decoded["alg"])
|
|
}
|
|
if decoded["url"] != "https://acme.example.com/acme/new-account" {
|
|
t.Errorf("url = %v", decoded["url"])
|
|
}
|
|
if decoded["nonce"] != "abc123-server-nonce" {
|
|
t.Errorf("nonce = %v", decoded["nonce"])
|
|
}
|
|
if decoded["jwk"] == nil {
|
|
t.Error("newAccount header must contain jwk")
|
|
}
|
|
if _, ok := decoded["kid"]; ok {
|
|
t.Error("newAccount header must not contain kid (mutually exclusive with jwk)")
|
|
}
|
|
|
|
// Verify signature is present.
|
|
if len(raw.Signature) == 0 {
|
|
t.Fatal("signature is empty")
|
|
}
|
|
|
|
// Verify RawJWT marshals as flattened JWS JSON.
|
|
flat, err := json.Marshal(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var flatMap map[string]string
|
|
if err := json.Unmarshal(flat, &flatMap); err != nil {
|
|
t.Fatalf("flattened JWS is not valid JSON: %v", err)
|
|
}
|
|
for _, field := range []string{"protected", "payload", "signature"} {
|
|
if flatMap[field] == "" {
|
|
t.Errorf("flattened JWS missing %q field", field)
|
|
}
|
|
}
|
|
|
|
// Round-trip: unmarshal flattened JWS back into a RawJWT.
|
|
var roundTrip jwt.RawJWT
|
|
if err := json.Unmarshal(flat, &roundTrip); err != nil {
|
|
t.Fatalf("unmarshal flattened JWS: %v", err)
|
|
}
|
|
if string(roundTrip.Protected) != string(raw.Protected) {
|
|
t.Error("round-trip: protected mismatch")
|
|
}
|
|
if string(roundTrip.Payload) != string(raw.Payload) {
|
|
t.Error("round-trip: payload mismatch")
|
|
}
|
|
if string(roundTrip.Signature) != string(raw.Signature) {
|
|
t.Error("round-trip: signature mismatch")
|
|
}
|
|
}
|
|
|
|
// TestAuthenticatedRequestJWS verifies ACME POST-as-GET: kid in header,
|
|
// jwk absent, empty payload.
|
|
func TestAuthenticatedRequestJWS(t *testing.T) {
|
|
pk, err := jwt.NewPrivateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signer, err := jwt.NewSigner([]*jwt.PrivateKey{pk})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// ACME kid is the account URL, not a key thumbprint.
|
|
// SignRaw uses the header's KID as-is (no conflict check).
|
|
accountURL := "https://acme.example.com/acme/acct/12345"
|
|
|
|
hdr := &AcmeHeader{
|
|
RFCHeader: jwt.RFCHeader{KID: accountURL},
|
|
URL: "https://acme.example.com/acme/orders",
|
|
Nonce: "def456-server-nonce",
|
|
}
|
|
|
|
// POST-as-GET: nil payload produces empty payload segment.
|
|
raw, err := signer.SignRaw(hdr, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify protected header.
|
|
headerJSON, err := base64.RawURLEncoding.DecodeString(string(raw.Protected))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(headerJSON, &decoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, ok := decoded["typ"]; ok {
|
|
t.Error("ACME header must not contain typ")
|
|
}
|
|
if decoded["kid"] != accountURL {
|
|
t.Errorf("kid = %v, want %s", decoded["kid"], accountURL)
|
|
}
|
|
if _, ok := decoded["jwk"]; ok {
|
|
t.Error("authenticated request must not contain jwk (mutually exclusive with kid)")
|
|
}
|
|
if decoded["url"] != "https://acme.example.com/acme/orders" {
|
|
t.Errorf("url = %v", decoded["url"])
|
|
}
|
|
|
|
// Verify empty payload produces valid flattened JWS.
|
|
flat, err := json.Marshal(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var flatMap map[string]string
|
|
if err := json.Unmarshal(flat, &flatMap); err != nil {
|
|
t.Fatalf("flattened JWS is not valid JSON: %v", err)
|
|
}
|
|
if flatMap["payload"] != "" {
|
|
t.Errorf("POST-as-GET payload should be empty, got %q", flatMap["payload"])
|
|
}
|
|
if flatMap["signature"] == "" {
|
|
t.Error("signature is empty")
|
|
}
|
|
}
|