diff --git a/LICENSES b/LICENSES index d27cf6dd..9787b744 100644 --- a/LICENSES +++ b/LICENSES @@ -73,6 +73,11 @@ github.com/hashicorp/errwrap,MPL-2.0 github.com/hashicorp/go-multierror,MPL-2.0 github.com/josharian/intern,MIT github.com/json-iterator/go,MIT +github.com/lestrrat-go/blackmagic,MIT +github.com/lestrrat-go/httpcc,MIT +github.com/lestrrat-go/httprc/v3,MIT +github.com/lestrrat-go/jwx/v3,MIT +github.com/lestrrat-go/option/v2,MIT github.com/mailru/easyjson,MIT github.com/mattn/go-colorable,MIT github.com/mattn/go-isatty,MIT diff --git a/go.mod b/go.mod index 0c074ebf..f1a7832f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/google/uuid v1.6.0 github.com/hashicorp/go-multierror v1.1.1 github.com/jetstack/venafi-connection-lib v0.5.2 + github.com/lestrrat-go/jwx/v3 v3.0.13 github.com/microcosm-cc/bluemonday v1.0.27 github.com/pmylund/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.23.2 @@ -32,6 +33,7 @@ require ( github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -39,6 +41,7 @@ require ( github.com/go-logr/zapr v1.3.0 // indirect github.com/go418/concurrentcache v0.6.0 // indirect github.com/go418/concurrentcache/logger v0.0.0-20250207095056-c0b7f8cc8bc2 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.0 // indirect @@ -46,8 +49,13 @@ require ( github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect + github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/segmentio/asm v1.2.1 // indirect github.com/sosodev/duration v1.3.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/vektah/gqlparser/v2 v2.5.30 // indirect @@ -59,7 +67,7 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.47.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect @@ -98,9 +106,9 @@ require ( github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.9.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 7a717d60..fe15b730 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= @@ -63,6 +65,8 @@ github.com/go418/concurrentcache v0.6.0 h1:36A7j+c0dChEAMotq+lBQwQPyI4CMCy5HgMCc github.com/go418/concurrentcache v0.6.0/go.mod h1:F498AylMP488QhU9KSE8VoN3u2FhGt7hXOgJ2CdvysM= github.com/go418/concurrentcache/logger v0.0.0-20250207095056-c0b7f8cc8bc2 h1:wVvBhfD+7srZ470Z06t5rp93faukGddvUJR4+owL0Kw= github.com/go418/concurrentcache/logger v0.0.0-20250207095056-c0b7f8cc8bc2/go.mod h1:DpmmUFByr4p8fGMbp2gsGJhqgcP1SXjyVZDiW0f8aSY= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -119,6 +123,20 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= +github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.2 h1:7u4HUaD0NQbf2/n5+fyp+T10hNCsAnwKfqn4A4Baif0= +github.com/lestrrat-go/httprc/v3 v3.0.2/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk= +github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU= +github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= +github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -158,6 +176,8 @@ github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlT github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= @@ -180,6 +200,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= +github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -229,8 +251,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -253,22 +275,22 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/envelope/doc.go b/internal/envelope/doc.go index 751b5bbd..403eb728 100644 --- a/internal/envelope/doc.go +++ b/internal/envelope/doc.go @@ -1,12 +1,13 @@ // Package envelope provides types and interfaces for envelope encryption. // // Envelope encryption combines asymmetric and symmetric cryptography to -// efficiently encrypt data. The EncryptedData type holds the result, and -// the Encryptor interface defines the encryption operation. +// efficiently encrypt data. The Encryptor interface defines the encryption +// operation, returning data in JWE (JSON Web Encryption) format as defined +// in RFC 7516. // // Implementations are available in subpackages: // -// - internal/envelope/rsa: RSA-OAEP + AES-256-GCM +// - internal/envelope/rsa: RSA-OAEP-256 + AES-256-GCM using JWE // // See subpackage documentation for usage examples. package envelope diff --git a/internal/envelope/rsa/doc.go b/internal/envelope/rsa/doc.go index 9817f844..750d3209 100644 --- a/internal/envelope/rsa/doc.go +++ b/internal/envelope/rsa/doc.go @@ -1,3 +1,11 @@ -// Package rsa implements RSA envelope encryption, conforming to the interface in the envelope package. -// It uses RSA-OAEP with SHA-256 for key encryption, and AES-256-GCM for data encryption. +// Package rsa implements RSA envelope encryption using JWE (JSON Web Encryption) format. +// It conforms to the interface in the envelope package. +// +// The implementation uses: +// - RSA-OAEP-256 (RSA-OAEP with SHA-256) for key encryption +// - AES-256-GCM (A256GCM) for content encryption +// - JWE Compact Serialization format as defined in RFC 7516 +// +// The output is a JWE string with 5 base64url-encoded parts separated by dots: +// header.encryptedKey.iv.ciphertext.tag package rsa diff --git a/internal/envelope/rsa/encryptor.go b/internal/envelope/rsa/encryptor.go index 7e29066c..8cc0e17a 100644 --- a/internal/envelope/rsa/encryptor.go +++ b/internal/envelope/rsa/encryptor.go @@ -1,41 +1,37 @@ package rsa import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" "crypto/rsa" - "crypto/sha256" "fmt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwe" + "github.com/jetstack/preflight/internal/envelope" ) const ( - // aesKeySize is the size of the AES-256 key in bytes; aes.NewCipher generates cipher.Block based - // on the size of key passed in, and 32 bytes corresponds to a 256-bit AES key - aesKeySize = 32 - // minRSAKeySize is the minimum RSA key size in bits; we'd expect that keys will be larger but 2048 is a sane floor // to enforce to ensure that a weak key can't accidentally be used minRSAKeySize = 2048 - // keyAlgorithmIdentifier is set in EncryptedData to identify the key wrapping algorithm used in this package - keyAlgorithmIdentifier = "RSA-OAEP-SHA256" + // EncryptionType is the type identifier for RSA JWE encryption + EncryptionType = "JWE-RSA" ) // Compile-time check that Encryptor implements envelope.Encryptor var _ envelope.Encryptor = (*Encryptor)(nil) -// Encryptor provides envelope encryption using RSA for key wrapping -// and AES-256-GCM for data encryption. +// Encryptor provides envelope encryption using RSA-OAEP-256 for key wrapping +// and AES-256-GCM for data encryption, outputting JWE Compact Serialization format. type Encryptor struct { - keyID string - rsaPublicKey *rsa.PublicKey + keyID string + publicKey *rsa.PublicKey } // NewEncryptor creates a new Encryptor with the provided RSA public key. -// The RSA key must be at least minRSAKeySize bits +// The RSA key must be at least minRSAKeySize bits. +// The encryptor will use RSA-OAEP-256 for key encryption and A256GCM for content encryption. func NewEncryptor(keyID string, publicKey *rsa.PublicKey) (*Encryptor, error) { if publicKey == nil { return nil, fmt.Errorf("RSA public key cannot be nil") @@ -52,77 +48,39 @@ func NewEncryptor(keyID string, publicKey *rsa.PublicKey) (*Encryptor, error) { } return &Encryptor{ - keyID: keyID, - rsaPublicKey: publicKey, + keyID: keyID, + publicKey: publicKey, }, nil } // Encrypt performs envelope encryption on the provided data. -// It generates a random AES-256 key, encrypts the data with AES-256-GCM, -// then encrypts the AES key with RSA-OAEP-SHA256. +// It returns an EncryptedData struct containing JWE Compact Serialization format and type metadata. +// The JWE uses RSA-OAEP-256 for key encryption and A256GCM for content encryption. func (e *Encryptor) Encrypt(data []byte) (*envelope.EncryptedData, error) { if len(data) == 0 { return nil, fmt.Errorf("data to encrypt cannot be empty") } - aesKey := make([]byte, aesKeySize) - if _, err := rand.Read(aesKey); err != nil { - return nil, fmt.Errorf("failed to generate AES key: %w", err) - } - - // zero the key from memory before the function returns - // TODO: in go1.26+, consider using secret.Do in this function - defer func() { - for i := range aesKey { - aesKey[i] = 0 - } - }() - - block, err := aes.NewCipher(aesKey) - if err != nil { - return nil, fmt.Errorf("failed to create AES cipher: %w", err) - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("failed to create GCM cipher: %w", err) - } - - encryptedData := &envelope.EncryptedData{ - KeyID: e.keyID, - KeyAlgorithm: keyAlgorithmIdentifier, - EncryptedKey: nil, - EncryptedData: nil, - Nonce: make([]byte, gcm.NonceSize()), - } - - // Generate a random nonce for AES-GCM. - // Security: Nonces must never be re-used for a given key. Since we generate a new AES key for each encryption, - // the risk of nonce reuse is not a concern here. - if _, err := rand.Read(encryptedData.Nonce); err != nil { - return nil, fmt.Errorf("failed to generate nonce: %w", err) + // Create headers with the key ID + headers := jwe.NewHeaders() + if err := headers.Set("kid", e.keyID); err != nil { + return nil, fmt.Errorf("failed to set key ID header: %w", err) } - // Seal encrypts and authenticates the data. This could include additional authenticated data, - // but we don't make use of that here. - // First nil: allocate new slice for output. - // Last nil: no additional authenticated data (AAD) needed. - - encryptedData.EncryptedData = gcm.Seal(nil, encryptedData.Nonce, data, nil) - - // Encrypt AES key with RSA-OAEP-SHA256. The nil parameter means no additional - // context data is mixed into the hash; this could be used to disambiguate different uses of the same key, - // but we only have one use for the key here. - encryptedData.EncryptedKey, err = rsa.EncryptOAEP( - sha256.New(), - rand.Reader, - e.rsaPublicKey, - aesKey, - nil, + // Encrypt using RSA-OAEP-256 for key algorithm and A256GCM for content encryption + // TODO: in go1.26+, consider using secret.Do to wrap this call, since it will generate an AES key + encrypted, err := jwe.Encrypt( + data, + jwe.WithKey(jwa.RSA_OAEP_256(), e.publicKey, jwe.WithPerRecipientHeaders(headers)), + jwe.WithContentEncryption(jwa.A256GCM()), + jwe.WithCompact(), ) if err != nil { - return nil, fmt.Errorf("failed to encrypt AES key with RSA: %w", err) + return nil, fmt.Errorf("failed to encrypt data: %w", err) } - return encryptedData, nil + return &envelope.EncryptedData{ + Data: encrypted, + Type: EncryptionType, + }, nil } diff --git a/internal/envelope/rsa/encryptor_test.go b/internal/envelope/rsa/encryptor_test.go index ede0ea3b..ac68a742 100644 --- a/internal/envelope/rsa/encryptor_test.go +++ b/internal/envelope/rsa/encryptor_test.go @@ -3,9 +3,13 @@ package rsa import ( "crypto/rand" "crypto/rsa" + "encoding/base64" + "strings" "sync" "testing" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwe" "github.com/stretchr/testify/require" ) @@ -103,21 +107,23 @@ func TestEncrypt_VariousDataSizes(t *testing.T) { result, err := enc.Encrypt(data) require.NoError(t, err) require.NotNil(t, result) + require.Equal(t, EncryptionType, result.Type, "Type should be JWE-RSA") - // Verify all fields are populated - require.NotEmpty(t, result.EncryptedKey) - require.NotEmpty(t, result.EncryptedData) - require.NotEmpty(t, result.Nonce) + // Verify JWE Compact Serialization format (5 base64url parts separated by dots) + jweString := string(result.Data) + parts := strings.Split(jweString, ".") + require.Len(t, parts, 5, "JWE Compact Serialization should have 5 parts") - // Verify KeyID and KeyAlgorithm are set correctly - require.Equal(t, testKeyID, result.KeyID) - require.Equal(t, keyAlgorithmIdentifier, result.KeyAlgorithm) + // Verify each part is non-empty + for i, part := range parts { + require.NotEmpty(t, part, "JWE part %d should not be empty", i) - // Verify nonce is correct size (12 bytes for GCM) - require.Len(t, result.Nonce, 12) + _, err = base64.RawURLEncoding.DecodeString(part) + require.NoError(t, err, "JWE part %d should be valid base64url: %s", i, part) + } - // Verify encrypted data differs from input - require.NotEqual(t, data, result.EncryptedData) + // Verify the result differs from input + require.NotEqual(t, data, result.Data) }) } } @@ -145,27 +151,17 @@ func TestEncrypt_NonDeterministic(t *testing.T) { // Encrypt the same data twice result1, err := enc.Encrypt(data) require.NoError(t, err) + require.Equal(t, EncryptionType, result1.Type, "Type should be JWE-RSA") result2, err := enc.Encrypt(data) require.NoError(t, err) + require.Equal(t, EncryptionType, result2.Type, "Type should be JWE-RSA") - // Verify KeyID and KeyAlgorithm are set correctly in both results - require.Equal(t, testKeyID, result1.KeyID) - require.Equal(t, keyAlgorithmIdentifier, result1.KeyAlgorithm) - require.Equal(t, testKeyID, result2.KeyID) - require.Equal(t, keyAlgorithmIdentifier, result2.KeyAlgorithm) - - // Nonces should be different (random) - require.NotEqual(t, result1.Nonce, result2.Nonce) - - // Encrypted data should be different due to different nonces - require.NotEqual(t, result1.EncryptedData, result2.EncryptedData) - - // Encrypted keys should be different due to RSA-OAEP randomness - require.NotEqual(t, result1.EncryptedKey, result2.EncryptedKey) + // Results should be different due to random nonces and RSA-OAEP randomness + require.NotEqual(t, result1.Data, result2.Data, "Encrypting the same data twice should produce different JWE outputs") } -func TestEncrypt_AllFieldsPopulated(t *testing.T) { +func TestEncrypt_JWEFormat(t *testing.T) { key := testKey() enc, err := NewEncryptor(testKeyID, &key.PublicKey) @@ -174,16 +170,31 @@ func TestEncrypt_AllFieldsPopulated(t *testing.T) { data := []byte("test data") result, err := enc.Encrypt(data) require.NoError(t, err) + require.Equal(t, EncryptionType, result.Type, "Type should be JWE-RSA") + + // Parse and decrypt the JWE to verify format and algorithms + decrypted, err := jwe.Decrypt(result.Data, jwe.WithKey(jwa.RSA_OAEP_256(), key)) + require.NoError(t, err, "Result should be valid JWE with RSA-OAEP-256 and A256GCM, and should decrypt successfully") + require.Equal(t, data, decrypted, "Decrypted data should match original") +} + +func TestEncrypt_DecryptRoundtrip(t *testing.T) { + key := testKey() + + enc, err := NewEncryptor(testKeyID, &key.PublicKey) + require.NoError(t, err) + + originalData := []byte("test data for roundtrip encryption and decryption") - require.NotNil(t, result) - require.NotEmpty(t, result.EncryptedKey, "EncryptedKey should be populated") - require.NotEmpty(t, result.EncryptedData, "EncryptedData should be populated") - require.NotEmpty(t, result.Nonce, "Nonce should be populated") + // Encrypt the data + encrypted, err := enc.Encrypt(originalData) + require.NoError(t, err) + require.Equal(t, EncryptionType, encrypted.Type, "Type should be JWE-RSA") - // Verify KeyID and KeyAlgorithm are set correctly - require.Equal(t, testKeyID, result.KeyID, "KeyID should match the encryptor's keyID") - require.Equal(t, keyAlgorithmIdentifier, result.KeyAlgorithm, "KeyAlgorithm should be the value of keyAlgorithmIdentifier") + // Decrypt using the private key + decrypted, err := jwe.Decrypt(encrypted.Data, jwe.WithKey(jwa.RSA_OAEP_256(), key)) + require.NoError(t, err, "Decryption should succeed with the correct private key") - // Verify encrypted key size is appropriate for RSA 2048 - require.Equal(t, 256, len(result.EncryptedKey), "EncryptedKey should be 256 bytes for RSA 2048") + // Verify the decrypted data matches the original + require.Equal(t, originalData, decrypted, "Decrypted data should match original data") } diff --git a/internal/envelope/types.go b/internal/envelope/types.go index 83fe5ea2..19b644bf 100644 --- a/internal/envelope/types.go +++ b/internal/envelope/types.go @@ -1,31 +1,16 @@ package envelope -// EncryptedData contains the result of envelope encryption. -// It includes the encrypted data, the encrypted symmetric key which was used for encrypting the original data, -// and the nonce needed for the symmetric decryption. +// EncryptedData represents encrypted data along with metadata about the encryption type. type EncryptedData struct { - // KeyID is the identifier of the asymmetric key used to encrypt the AES key. - KeyID string `json:"key_id"` - - // KeyAlgorithm is the algorithm of the asymmetric key used to encrypt the AES key. - KeyAlgorithm string `json:"key_algorithm"` - - // EncryptedKey is an encrypted AES-256-GCM symmetric key, used to encrypt EncryptedData. - // This is ciphertext and should only be decryptable by the holder of the private key. - EncryptedKey []byte `json:"encrypted_key"` - - // EncryptedData is the actual data encrypted using the AES-256-GCM in EncryptedKey. - // This is ciphertext and requires the decrypted AES key and nonce for decryption. - EncryptedData []byte `json:"encrypted_data"` - - // Nonce is the 12-byte nonce used for AES-GCM encryption. - // This is intentionally plaintext. - Nonce []byte `json:"nonce"` + // Data contains the encrypted payload + Data []byte + // Type indicates the encryption format (e.g., "JWE-RSA") + Type string } // Encryptor performs envelope encryption on arbitrary data. type Encryptor interface { - // Encrypt encrypts data using envelope encryption, returning the resulting data along - // with identifiers of the asymmetric key used to encrypt the AES key. + // Encrypt encrypts data using envelope encryption, returning an EncryptedData struct + // containing the encrypted payload and encryption type metadata. Encrypt(data []byte) (*EncryptedData, error) }