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>
This commit is contained in:
152
refimpl/go-lang/ect/create.go
Normal file
152
refimpl/go-lang/ect/create.go
Normal file
@@ -0,0 +1,152 @@
|
||||
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 5–15 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)
|
||||
}
|
||||
Reference in New Issue
Block a user