Files
ietf-wimse-ect/refimpl/go-lang/ect/create.go
Christian Nennemann 884d2dc836 feat: migrate refimpls from draft-00 to draft-01 claim names
- Rename `par` to `pred` (predecessor) in types, serialization, tests
- Remove `pol`, `pol_decision` from core payload; move to `ect_ext`
- Remove `sub` from payload (not part of ECT spec)
- Update `typ` from `wimse-exec+jwt` to `exec+jwt` (accept both)
- Rename MaxParLength to MaxPredLength everywhere
- Update testdata, demos, READMEs with migration table
- All Go tests pass, all 56 Python tests pass (90% coverage)
2026-04-03 10:55:58 +02:00

141 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ect
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"errors"
"time"
"github.com/go-jose/go-jose/v4"
)
// CreateOptions configures ECT creation.
type CreateOptions struct {
// KeyID is the kid header value (references public key from WIT).
KeyID string
// IATMaxAge caps how far in the past iat may be (recommended 15 min).
IATMaxAge time.Duration
// DefaultExpiry is added to time.Now() for exp when zero (recommended 515 min).
DefaultExpiry time.Duration
// ValidateUUIDs when true requires jti and wid (if set) to be UUID format (RFC 9562).
ValidateUUIDs bool
// MaxPredLength is the max number of predecessor references (0 = no limit; recommended 100).
MaxPredLength int
}
// DefaultCreateOptions returns recommended defaults.
func DefaultCreateOptions() CreateOptions {
return CreateOptions{
IATMaxAge: 15 * time.Minute,
DefaultExpiry: 10 * time.Minute,
}
}
// Create builds and signs an ECT. Payload must have required claims set;
// Iat/Exp can be zero to use defaults (now, now+DefaultExpiry).
// Create may modify the payload in place (Iat, Exp, Sub, Par) when filling defaults; pass a copy if the original must stay unchanged.
func Create(payload *Payload, privateKey *ecdsa.PrivateKey, opts CreateOptions) (compact string, err error) {
if payload == nil || privateKey == nil {
return "", ErrPayloadRequired
}
if opts.KeyID == "" {
return "", ErrKeyIDRequired
}
now := time.Now()
if payload.Iat == 0 {
payload.Iat = now.Unix()
}
if payload.Exp == 0 {
if opts.DefaultExpiry == 0 {
opts.DefaultExpiry = 10 * time.Minute
}
payload.Exp = now.Add(opts.DefaultExpiry).Unix()
}
if payload.Pred == nil {
payload.Pred = []string{}
}
if err := validatePayloadForCreate(payload, opts); err != nil {
return "", err
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return "", err
}
sig := jose.SigningKey{Algorithm: jose.ES256, Key: privateKey}
options := &jose.SignerOptions{
ExtraHeaders: map[jose.HeaderKey]interface{}{
jose.HeaderType: ECTType,
jose.HeaderKey("kid"): opts.KeyID,
},
}
signer, err := jose.NewSigner(sig, options)
if err != nil {
return "", err
}
jws, err := signer.Sign(payloadBytes)
if err != nil {
return "", err
}
return jws.CompactSerialize()
}
func validatePayloadForCreate(p *Payload, opts CreateOptions) error {
if p.Iss == "" {
return errors.New("ect: iss required")
}
if len(p.Aud) == 0 {
return errors.New("ect: aud required")
}
if p.Jti == "" {
return errors.New("ect: jti required")
}
if p.ExecAct == "" {
return errors.New("ect: exec_act required")
}
if opts.ValidateUUIDs {
if !ValidUUID(p.Jti) {
return ErrInvalidJTI
}
if p.Wid != "" && !ValidUUID(p.Wid) {
return ErrInvalidWID
}
}
if opts.MaxPredLength > 0 && len(p.Pred) > opts.MaxPredLength {
return ErrPredLength
}
if p.InpHash != "" {
if err := ValidateHashFormat(p.InpHash); err != nil {
return err
}
}
if p.OutHash != "" {
if err := ValidateHashFormat(p.OutHash); err != nil {
return err
}
}
if err := ValidateExt(p.Ext); err != nil {
return err
}
// compensation_* live in ext per spec
if p.Ext != nil {
if _, hasReason := p.Ext["compensation_reason"]; hasReason {
if v, _ := p.Ext["compensation_required"].(bool); !v {
return errors.New("ect: ext.compensation_reason requires ext.compensation_required true")
}
}
}
return nil
}
// GenerateKey creates an ECDSA P-256 key for ES256 (for testing/demo).
func GenerateKey() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}