Files
ietf-wimse-ect/refimpl/go-lang/ect/create.go
Christian Nennemann bbf557e54b Restructure refimpl into go-lang and python subdirectories
Move Go reference implementation to refimpl/go-lang/ and add new
Python reference implementation in refimpl/python/. Update build.sh
with renamed draft and simplified tool paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:11:55 +01:00

153 lines
3.9 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
// MaxParLength is the max number of parent references (0 = no limit; recommended 100).
MaxParLength 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.Sub == "" {
payload.Sub = payload.Iss
}
if payload.Par == nil {
payload.Par = []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.MaxParLength > 0 && len(p.Par) > opts.MaxParLength {
return ErrParLength
}
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
}
// pol/pol_decision are OPTIONAL; if either is set, both must be present and valid
if p.Pol != "" || p.PolDecision != "" {
if p.Pol == "" || p.PolDecision == "" {
return ErrPolPolDecisionPair
}
if !ValidPolDecision(p.PolDecision) {
return ErrInvalidPolDecision
}
}
// 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)
}