Files
ietf-wimse-ect/refimpl/ect/create.go
Christian Nennemann f9357fdf88 Add WIMSE ECT reference implementation (Go)
- ect library: create, verify, DAG validation, ledger interface
- In-memory ledger and ECTStore for full ledger mode
- Test vectors and unit tests; two-agent demo (cmd/demo)
- README: document refimpl scope and usage

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 22:05:30 +01:00

119 lines
2.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
// NotBeforeIATWindow 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
}
// 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).
func Create(payload *Payload, privateKey *ecdsa.PrivateKey, opts CreateOptions) (compact string, err error) {
if payload == nil || privateKey == nil {
return "", errors.New("ect: payload and privateKey required")
}
if opts.KeyID == "" {
return "", errors.New("ect: KeyID required")
}
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); 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) 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.Tid == "" {
return errors.New("ect: tid required")
}
if p.ExecAct == "" {
return errors.New("ect: exec_act required")
}
if p.Pol == "" {
return errors.New("ect: pol required")
}
if !ValidPolDecision(p.PolDecision) {
return errors.New("ect: pol_decision must be approved, rejected, or pending_human_review")
}
if p.CompensationReason != "" && !p.CompensationRequired {
return errors.New("ect: compensation_reason requires 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)
}