├── .assets └── logo.png ├── .github └── workflows │ ├── codeql-analysis.yml │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── hpke.go └── hpke_test.go /.assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedisct1/go-hpke-compact/5caa4621366f9e3ced71dec895fe9bb16ec0a16f/.assets/logo.png -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '29 12 * * 1' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | language: [ 'go' ] 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | 25 | # Initializes the CodeQL tools for scanning. 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v1 28 | with: 29 | languages: ${{ matrix.language }} 30 | 31 | - name: Autobuild 32 | uses: github/codeql-action/autobuild@v1 33 | 34 | # ℹ️ Command-line programs to run using the OS shell. 35 | # 📚 https://git.io/JvXDl 36 | 37 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 38 | # and modify them (or add more) to build your code if your project 39 | # uses a compiled language 40 | 41 | #- run: | 42 | # make bootstrap 43 | # make release 44 | 45 | - name: Perform CodeQL Analysis 46 | uses: github/codeql-action/analyze@v1 47 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v4 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: go build -v ./... 34 | 35 | - name: Test 36 | run: go test -v ./... 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | go.sum 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * ISC License 3 | * 4 | * Copyright (c) 2020-2024 5 | * Frank Denis 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 14 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 17 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI status](https://github.com/jedisct1/go-hpke-compact/workflows/Go/badge.svg)](https://github.com/jedisct1/go-hpke-compact/actions) 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/jedisct1/go-hpke-compact.svg)](https://pkg.go.dev/github.com/jedisct1/go-hpke-compact) 3 | 4 | # ![HPKE-Compact](.assets/logo.png) 5 | 6 | # A compact HPKE implemention for Go 7 | 8 | `hpkecompact` is a small implementation of the [Hybrid Public Key Encryption](https://cfrg.github.io/draft-irtf-cfrg-hpke/draft-irtf-cfrg-hpke.html) (HPKE) draft. 9 | 10 | It fits in a single file and only uses the Go standard library and `x/crypto`. 11 | 12 | Suites are currently limited to `X25519-HKDF-SHA256` / `HKDF-SHA-256` / `{AES-{128,256}-GCM, CHACHA20-POLY1305}`; these are very likely to be the most commonly deployed ones for a forseable future. 13 | 14 | ## Usage 15 | 16 | ### Suite instantiation 17 | 18 | ```go 19 | suite, err := NewSuite(KemX25519HkdfSha256, KdfHkdfSha256, AeadAes128Gcm) 20 | ``` 21 | 22 | ### Key pair creation 23 | 24 | ```go 25 | serverKp, err := ctx.GenerateKeyPair() 26 | ``` 27 | 28 | ### Client: creation and encapsulation of the shared secret 29 | 30 | A _client_ initiates a connexion by sending an encrypted secret; a _server_ accepts an encrypted secret from a client, and decrypts it, so that both parties can eventually agree on a shared secret. 31 | 32 | ```go 33 | clientCtx, encryptedSharedSecret, err := 34 | suite.NewClientContext(serverKp.PublicKey, []byte("application name"), nil) 35 | ``` 36 | 37 | * `encryptedSharedSecret` needs to be sent to the server. 38 | * `clientCtx` can be used to encrypt/decrypt messages exchanged with the server. 39 | * The last parameter is an optional pre-shared key (`Psk` type). 40 | 41 | To improve misuse resistance, this implementation uses distinct types for the client and the server context: `ClientContext` for the client, and `ServerContext` for the server. 42 | 43 | ### Server: decapsulation of the shared secret 44 | 45 | ```go 46 | serverCtx, err := suite.NewServerContext(encryptedSharedSecret, 47 | serverKp, []byte("application name"), nil) 48 | ``` 49 | 50 | * `serverCtx` can be used to encrypt/decrypt messages exchanged with the client 51 | * The last parameter is an optional pre-shared key (`Psk` type). 52 | 53 | ### Encryption of a message from the client to the server 54 | 55 | A message can be encrypted by the client for the server: 56 | 57 | ```go 58 | ciphertext, err := clientCtx.EncryptToServer([]byte("message"), nil) 59 | ``` 60 | 61 | Nonces are automatically incremented, so it is safe to call this function multiple times within the same context. 62 | 63 | Second parameter is optional associated data. 64 | 65 | ### Decryption of a ciphertext received by the server 66 | 67 | The server can decrypt a ciphertext sent by the client: 68 | 69 | ```go 70 | decrypted, err := serverCtx.DecryptFromClient(ciphertext, nil) 71 | ``` 72 | 73 | Second parameter is optional associated data. 74 | 75 | ### Encryption of a message from the server to the client 76 | 77 | A message can also be encrypted by the server for the client: 78 | 79 | ```go 80 | ciphertext, err := serverCtx.EncryptToClient([]byte("response"), nil) 81 | ``` 82 | 83 | Nonces are automatically incremented, so it is safe to call this function multiple times within the same context. 84 | 85 | Second parameter is optional associated data. 86 | 87 | ### Decryption of a ciphertext received by the client 88 | 89 | The client can decrypt a ciphertext sent by the server: 90 | 91 | ```go 92 | decrypted, err := clientCtx.DecryptFromServer(ciphertext, nil) 93 | ``` 94 | 95 | Second parameter is optional associated data. 96 | 97 | ## Authenticated modes 98 | 99 | Authenticated modes, with or without a PSK are supported. 100 | 101 | Just replace `NewClientContext()` with `NewAuthenticatedClientContext()` and `NewServerContext()` with `NewAuthenticatedServerContext()` for authentication. 102 | 103 | ```go 104 | clientKp, err := suite.GenerateKeyPair() 105 | serverKp, err := suite.GenerateKeyPair() 106 | 107 | clientCtx, encryptedSharedSecret, err := suite.NewAuthenticatedClientContext( 108 | clientKp, serverKp.PublicKey, []byte("app"), psk) 109 | 110 | serverCtx, err := suite.NewAuthenticatedServerContext( 111 | clientKp.PublicKey, encryptedSharedSecret, serverKp, []byte("app"), psk) 112 | ``` 113 | 114 | ### Exporter secret 115 | 116 | The exporter secret can be obtained with the `ExportedSecret()` function available both in the `ServerContext` and `ClientContext` structures: 117 | 118 | ```go 119 | exporter := serverCtx.ExporterSecret() 120 | ``` 121 | 122 | ### Key derivation 123 | 124 | ```go 125 | secret1, err := clientCtx.Export("description 1") 126 | secret2, err := serverCtx.Export("description 2"); 127 | ``` 128 | 129 | ### Access the raw cipher interface 130 | 131 | ```go 132 | cipher, err := suite.NewRawCipher(key) 133 | ``` 134 | 135 | ## That's it! 136 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jedisct1/go-hpke-compact 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/powerman/check v1.8.0 7 | golang.org/x/crypto v0.31.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/golang/protobuf v1.5.3 // indirect 13 | github.com/pkg/errors v0.9.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | github.com/powerman/deepequal v0.1.0 // indirect 16 | github.com/smartystreets/goconvey v1.8.1 // indirect 17 | golang.org/x/sys v0.28.0 // indirect 18 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 19 | google.golang.org/grpc v1.56.3 // indirect 20 | google.golang.org/protobuf v1.34.2 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /hpke.go: -------------------------------------------------------------------------------- 1 | package hpkecompact 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | crypto_rand "crypto/rand" 7 | "crypto/sha256" 8 | "encoding/binary" 9 | "errors" 10 | "hash" 11 | 12 | "golang.org/x/crypto/chacha20poly1305" 13 | "golang.org/x/crypto/curve25519" 14 | "golang.org/x/crypto/hkdf" 15 | ) 16 | 17 | var hpkeVersion = [7]byte{'H', 'P', 'K', 'E', '-', 'v', '1'} 18 | 19 | // Mode - Mode 20 | type Mode byte 21 | 22 | const ( 23 | // ModeBase - Base mode 24 | ModeBase Mode = 0x00 25 | // ModePsk - PSK mode 26 | ModePsk Mode = 0x01 27 | // ModeAuth - Auth mode 28 | ModeAuth Mode = 0x02 29 | // ModeAuthPsk - PSK Auth mode 30 | ModeAuthPsk Mode = 0x03 31 | ) 32 | 33 | // KemID - KEM ID 34 | type KemID uint16 35 | 36 | const ( 37 | // KemX25519HkdfSha256 - X25519 with HKDF-SHA256 38 | KemX25519HkdfSha256 KemID = 0x0020 39 | ) 40 | 41 | // KdfID - KDF ID 42 | type KdfID uint16 43 | 44 | const ( 45 | // KdfHkdfSha256 - HKDF-SHA256 46 | KdfHkdfSha256 KdfID = 0x0001 47 | ) 48 | 49 | // AeadID - AEAD ID 50 | type AeadID uint16 51 | 52 | const ( 53 | // AeadAes128Gcm - AES128-GCM 54 | AeadAes128Gcm AeadID = 0x0001 55 | // AeadAes256Gcm - AES256-GCM 56 | AeadAes256Gcm AeadID = 0x0002 57 | // AeadChaCha20Poly1305 - ChaCha20-Poly1305 58 | AeadChaCha20Poly1305 AeadID = 0x0003 59 | // AeadExportOnly - Don't use the HPKE encryption API 60 | AeadExportOnly AeadID = 0xffff 61 | ) 62 | 63 | // Psk - Pre-shared key and key ID 64 | type Psk struct { 65 | Key []byte 66 | ID []byte 67 | } 68 | 69 | // KeyPair - A key pair (packed as a byte string) 70 | type KeyPair struct { 71 | // PublicKey - Public key 72 | PublicKey []byte 73 | // SecretKey - Secret key 74 | SecretKey []byte 75 | } 76 | 77 | type aeadState struct { 78 | aead aeadImpl 79 | baseNonce []byte 80 | counter []byte 81 | } 82 | 83 | // Suite - HPKE suite 84 | type Suite struct { 85 | SuiteIDContext [10]byte 86 | SuiteIDKEM [5]byte 87 | Hash func() hash.Hash 88 | PrkBytes uint16 89 | KeyBytes uint16 90 | NonceBytes uint16 91 | KemHashBytes uint16 92 | AeadID AeadID 93 | } 94 | 95 | // NewSuite - Create a new suite from its components 96 | func NewSuite(kemID KemID, kdfID KdfID, aeadID AeadID) (*Suite, error) { 97 | if kemID != KemX25519HkdfSha256 || kdfID != KdfHkdfSha256 { 98 | return nil, errors.New("unimplemented suite") 99 | } 100 | hash := sha256.New 101 | nonceBytes := uint16(12) 102 | var keyBytes uint16 103 | switch aeadID { 104 | case AeadAes128Gcm: 105 | keyBytes = 16 106 | case AeadAes256Gcm: 107 | keyBytes = 32 108 | case AeadChaCha20Poly1305: 109 | keyBytes = 32 110 | case AeadExportOnly: 111 | keyBytes = 0 112 | nonceBytes = 0 113 | default: 114 | return nil, errors.New("unimplemented suite") 115 | } 116 | var prkBytes uint16 117 | switch kdfID { 118 | case KdfHkdfSha256: 119 | prkBytes = 32 120 | default: 121 | return nil, errors.New("unimplemented suite") 122 | } 123 | var kemHashBytes uint16 124 | switch kemID { 125 | case KemX25519HkdfSha256: 126 | kemHashBytes = 32 127 | default: 128 | return nil, errors.New("unimplemented suite") 129 | } 130 | suite := Suite{ 131 | SuiteIDContext: getSuiteIDContext(kemID, kdfID, aeadID), 132 | SuiteIDKEM: getSuiteIDKEM(kemID), 133 | Hash: hash, 134 | KeyBytes: keyBytes, 135 | PrkBytes: prkBytes, 136 | NonceBytes: nonceBytes, 137 | KemHashBytes: kemHashBytes, 138 | AeadID: aeadID, 139 | } 140 | return &suite, nil 141 | } 142 | 143 | func getSuiteIDContext(kemID KemID, kdfID KdfID, aeadID AeadID) [10]byte { 144 | suiteIDContext := [10]byte{'H', 'P', 'K', 'E', 0, 0, 0, 0, 0, 0} 145 | binary.BigEndian.PutUint16(suiteIDContext[4:6], uint16(kemID)) 146 | binary.BigEndian.PutUint16(suiteIDContext[6:8], uint16(kdfID)) 147 | binary.BigEndian.PutUint16(suiteIDContext[8:10], uint16(aeadID)) 148 | return suiteIDContext 149 | } 150 | 151 | func getSuiteIDKEM(kemID KemID) [5]byte { 152 | suiteIDKEM := [5]byte{'K', 'E', 'M', 0, 0} 153 | binary.BigEndian.PutUint16(suiteIDKEM[3:5], uint16(kemID)) 154 | return suiteIDKEM 155 | } 156 | 157 | // Extract - KDF-Extract 158 | func (suite *Suite) Extract(secret []byte, salt []byte) []byte { 159 | return hkdf.Extract(suite.Hash, secret, salt) 160 | } 161 | 162 | // Expand - KDF-Expand 163 | func (suite *Suite) Expand(prk []byte, info []byte, length uint16) ([]byte, error) { 164 | reader := hkdf.Expand(suite.Hash, prk, info) 165 | out := make([]byte, length) 166 | if readNb, err := reader.Read(out); err != nil { 167 | return nil, err 168 | } else if readNb != int(length) { 169 | return nil, errors.New("unable to expand") 170 | } 171 | return out, nil 172 | } 173 | 174 | func (suite *Suite) labeledExtract(suiteID []byte, salt []byte, label string, ikm []byte) []byte { 175 | secret := append(hpkeVersion[:], suiteID...) 176 | secret = append(secret, []byte(label)...) 177 | secret = append(secret, ikm...) 178 | return suite.Extract(secret, salt) 179 | } 180 | 181 | func (suite *Suite) labeledExpand(suiteID []byte, prk []byte, label string, info []byte, length uint16) ([]byte, error) { 182 | labeledInfo := []byte{0, 0} 183 | binary.BigEndian.PutUint16(labeledInfo, length) 184 | labeledInfo = append(labeledInfo, hpkeVersion[:]...) 185 | labeledInfo = append(labeledInfo, suiteID...) 186 | labeledInfo = append(labeledInfo, []byte(label)...) 187 | labeledInfo = append(labeledInfo, info...) 188 | return suite.Expand(prk, labeledInfo, length) 189 | } 190 | 191 | func (suite *Suite) newAeadState(key []uint8, baseNonce []uint8) (*aeadState, error) { 192 | var aead aeadImpl 193 | var err error 194 | switch suite.AeadID { 195 | case AeadAes128Gcm, AeadAes256Gcm: 196 | aead, err = newAesAead(key) 197 | case AeadChaCha20Poly1305: 198 | aead, err = newChaChaPolyAead(key) 199 | default: 200 | return nil, errors.New("unimplemented AEAD") 201 | } 202 | if err != nil { 203 | return nil, err 204 | } 205 | return &aeadState{aead: aead, baseNonce: baseNonce, counter: make([]byte, suite.NonceBytes)}, nil 206 | } 207 | 208 | func verifyPskInputs(mode Mode, psk *Psk) error { 209 | if psk != nil && ((len(psk.Key) == 0) != (len(psk.ID) == 0)) { 210 | return errors.New("a PSK and a PSK ID need both to be set") 211 | } 212 | if psk != nil { 213 | if mode == ModeBase || mode == ModeAuth { 214 | return errors.New("PSK input provided when not needed") 215 | } 216 | } else if mode == ModePsk || mode == ModeAuthPsk { 217 | return errors.New("PSK required for that mode") 218 | } 219 | return nil 220 | } 221 | 222 | // innerContext - An AEAD context 223 | type innerContext struct { 224 | suite *Suite 225 | exporterSecret []byte 226 | outboundState *aeadState 227 | inboundState *aeadState 228 | } 229 | 230 | func (inner *innerContext) export(exporterContext []byte, length uint16) ([]byte, error) { 231 | return inner.suite.labeledExpand(inner.suite.SuiteIDContext[:], inner.exporterSecret, "sec", exporterContext, length) 232 | } 233 | 234 | // ClientContext - A client encryption context 235 | type ClientContext struct { 236 | inner innerContext 237 | } 238 | 239 | // ServerContext - A server encryption context 240 | type ServerContext struct { 241 | inner innerContext 242 | } 243 | 244 | func (suite *Suite) keySchedule(mode Mode, dhSecret []byte, info []byte, psk *Psk) (innerContext, error) { 245 | if err := verifyPskInputs(mode, psk); err != nil { 246 | return innerContext{}, err 247 | } 248 | if psk == nil { 249 | psk = &Psk{} 250 | } 251 | pskIDHash := suite.labeledExtract(suite.SuiteIDContext[:], nil, "psk_id_hash", psk.ID) 252 | infoHash := suite.labeledExtract(suite.SuiteIDContext[:], nil, "info_hash", info) 253 | keyScheduleContext := []byte{byte(mode)} 254 | keyScheduleContext = append(keyScheduleContext, pskIDHash...) 255 | keyScheduleContext = append(keyScheduleContext, infoHash...) 256 | secret := suite.labeledExtract(suite.SuiteIDContext[:], dhSecret, "secret", psk.Key) 257 | 258 | exporterSecret, err := suite.labeledExpand(suite.SuiteIDContext[:], secret, "exp", keyScheduleContext, suite.PrkBytes) 259 | if err != nil { 260 | return innerContext{}, err 261 | } 262 | 263 | var outboundState *aeadState 264 | if suite.AeadID != AeadExportOnly { 265 | outboundKey, err := suite.labeledExpand(suite.SuiteIDContext[:], secret, "key", keyScheduleContext, suite.KeyBytes) 266 | if err != nil { 267 | return innerContext{}, err 268 | } 269 | outboundBaseNonce, err := suite.labeledExpand(suite.SuiteIDContext[:], secret, "base_nonce", keyScheduleContext, suite.NonceBytes) 270 | if err != nil { 271 | return innerContext{}, err 272 | } 273 | outboundState, err = suite.newAeadState(outboundKey, outboundBaseNonce) 274 | if err != nil { 275 | return innerContext{}, err 276 | } 277 | } 278 | return innerContext{ 279 | suite: suite, 280 | exporterSecret: exporterSecret, 281 | outboundState: outboundState, 282 | }, nil 283 | } 284 | 285 | // GenerateKeyPair - Generate a random key pair 286 | func (suite *Suite) GenerateKeyPair() (KeyPair, error) { 287 | var pk, sk [32]byte 288 | if _, err := crypto_rand.Read(sk[:]); err != nil { 289 | return KeyPair{}, err 290 | } 291 | curve25519.ScalarBaseMult(&pk, &sk) 292 | return KeyPair{PublicKey: pk[:], SecretKey: sk[:]}, nil 293 | } 294 | 295 | // DeterministicKeyPair - Derive a deterministic key pair from a seed 296 | func (suite *Suite) DeterministicKeyPair(seed []byte) (KeyPair, error) { 297 | var pk, sk [32]byte 298 | prk := suite.labeledExtract(suite.SuiteIDKEM[:], nil, "dkp_prk", seed) 299 | xsk, err := suite.labeledExpand(suite.SuiteIDKEM[:], prk, "sk", nil, 32) 300 | if err != nil { 301 | return KeyPair{}, err 302 | } 303 | copy(sk[:], xsk) 304 | curve25519.ScalarBaseMult(&pk, &sk) 305 | return KeyPair{PublicKey: pk[:], SecretKey: sk[:]}, nil 306 | } 307 | 308 | func (suite *Suite) dh(pk []byte, sk []byte) ([]byte, error) { 309 | dhSecret, err := curve25519.X25519(sk, pk) 310 | if err != nil { 311 | return nil, err 312 | } 313 | return dhSecret, nil 314 | } 315 | 316 | func (suite *Suite) extractAndExpandDH(dh []byte, kemContext []byte) ([]byte, error) { 317 | prk := suite.labeledExtract(suite.SuiteIDKEM[:], nil, "eae_prk", dh) 318 | dhSecret, err := suite.labeledExpand(suite.SuiteIDKEM[:], prk, "shared_secret", kemContext, suite.KemHashBytes) 319 | if err != nil { 320 | return nil, err 321 | } 322 | return dhSecret, nil 323 | } 324 | 325 | func (suite *Suite) encap(serverPk []byte, seed []byte) ([]byte, []byte, error) { 326 | var ephKp KeyPair 327 | var err error 328 | if len(seed) > 0 { 329 | ephKp, err = suite.DeterministicKeyPair(seed) 330 | } else { 331 | ephKp, err = suite.GenerateKeyPair() 332 | } 333 | if err != nil { 334 | return nil, nil, err 335 | } 336 | dh, err := suite.dh(serverPk, ephKp.SecretKey) 337 | if err != nil { 338 | return nil, nil, err 339 | } 340 | kemContext := append(ephKp.PublicKey, serverPk...) 341 | dhSecret, err := suite.extractAndExpandDH(dh, kemContext) 342 | if err != nil { 343 | return nil, nil, err 344 | } 345 | return dhSecret, ephKp.PublicKey, nil 346 | } 347 | 348 | func (suite *Suite) decap(ephPk []byte, serverKp KeyPair) ([]byte, error) { 349 | dh, err := suite.dh(ephPk, serverKp.SecretKey) 350 | if err != nil { 351 | return nil, err 352 | } 353 | kemContext := append(ephPk, serverKp.PublicKey...) 354 | dhSecret, err := suite.extractAndExpandDH(dh, kemContext) 355 | if err != nil { 356 | return nil, err 357 | } 358 | return dhSecret, nil 359 | } 360 | 361 | func (suite *Suite) authEncap(serverPk []byte, clientKp KeyPair, seed []byte) ([]byte, []byte, error) { 362 | var ephKp KeyPair 363 | var err error 364 | if len(seed) > 0 { 365 | ephKp, err = suite.DeterministicKeyPair(seed) 366 | } else { 367 | ephKp, err = suite.GenerateKeyPair() 368 | } 369 | if err != nil { 370 | return nil, nil, err 371 | } 372 | dh1, err := suite.dh(serverPk, ephKp.SecretKey) 373 | if err != nil { 374 | return nil, nil, err 375 | } 376 | dh2, err := suite.dh(serverPk, clientKp.SecretKey) 377 | if err != nil { 378 | return nil, nil, err 379 | } 380 | dh := append(dh1, dh2...) 381 | kemContext := append(ephKp.PublicKey, serverPk...) 382 | kemContext = append(kemContext, clientKp.PublicKey...) 383 | dhSecret, err := suite.extractAndExpandDH(dh, kemContext) 384 | if err != nil { 385 | return nil, nil, err 386 | } 387 | return dhSecret, ephKp.PublicKey, nil 388 | } 389 | 390 | func (suite *Suite) authDecap(ephPk []byte, serverKp KeyPair, clientPk []byte) ([]byte, error) { 391 | dh1, err := suite.dh(ephPk, serverKp.SecretKey) 392 | if err != nil { 393 | return nil, err 394 | } 395 | dh2, err := suite.dh(clientPk, serverKp.SecretKey) 396 | if err != nil { 397 | return nil, err 398 | } 399 | dh := append(dh1, dh2...) 400 | kemContext := append(ephPk, serverKp.PublicKey...) 401 | kemContext = append(kemContext, clientPk...) 402 | dhSecret, err := suite.extractAndExpandDH(dh, kemContext) 403 | if err != nil { 404 | return nil, err 405 | } 406 | return dhSecret, nil 407 | } 408 | 409 | // NewClientContext - Create a new context for a client (aka "sender") 410 | func (suite *Suite) NewClientContext(serverPk []byte, info []byte, psk *Psk) (ClientContext, []byte, error) { 411 | dhSecret, enc, err := suite.encap(serverPk, nil) 412 | if err != nil { 413 | return ClientContext{}, nil, err 414 | } 415 | mode := ModeBase 416 | if psk != nil { 417 | mode = ModePsk 418 | } 419 | context, err := suite.keySchedule(mode, dhSecret, info, psk) 420 | if err != nil { 421 | return ClientContext{}, nil, err 422 | } 423 | return ClientContext{inner: context}, enc, nil 424 | } 425 | 426 | // NewClientDeterministicContext - Create a new deterministic context for a client - Should only be used for testing purposes 427 | func (suite *Suite) NewClientDeterministicContext(serverPk []byte, info []byte, psk *Psk, seed []byte) (ClientContext, []byte, error) { 428 | dhSecret, enc, err := suite.encap(serverPk, seed) 429 | if err != nil { 430 | return ClientContext{}, nil, err 431 | } 432 | mode := ModeBase 433 | if psk != nil { 434 | mode = ModePsk 435 | } 436 | context, err := suite.keySchedule(mode, dhSecret, info, psk) 437 | if err != nil { 438 | return ClientContext{}, nil, err 439 | } 440 | return ClientContext{inner: context}, enc, nil 441 | } 442 | 443 | // NewServerContext - Create a new context for a server (aka "recipient") 444 | func (suite *Suite) NewServerContext(enc []byte, serverKp KeyPair, info []byte, psk *Psk) (ServerContext, error) { 445 | dhSecret, err := suite.decap(enc, serverKp) 446 | if err != nil { 447 | return ServerContext{}, err 448 | } 449 | mode := ModeBase 450 | if psk != nil { 451 | mode = ModePsk 452 | } 453 | context, err := suite.keySchedule(mode, dhSecret, info, psk) 454 | if err != nil { 455 | return ServerContext{}, err 456 | } 457 | return ServerContext{inner: context}, nil 458 | } 459 | 460 | // NewAuthenticatedClientContext - Create a new context for a client (aka "sender"), with authentication 461 | func (suite *Suite) NewAuthenticatedClientContext(clientKp KeyPair, serverPk []byte, info []byte, psk *Psk) (ClientContext, []byte, error) { 462 | dhSecret, enc, err := suite.authEncap(serverPk, clientKp, nil) 463 | if err != nil { 464 | return ClientContext{}, nil, err 465 | } 466 | mode := ModeAuth 467 | if psk != nil { 468 | mode = ModeAuthPsk 469 | } 470 | context, err := suite.keySchedule(mode, dhSecret, info, psk) 471 | if err != nil { 472 | return ClientContext{}, nil, err 473 | } 474 | return ClientContext{inner: context}, enc, nil 475 | } 476 | 477 | // NewAuthenticatedClientDeterministicContext - Create a new deterministic context for a client, with authentication - Should only be used for testing purposes 478 | func (suite *Suite) NewAuthenticatedClientDeterministicContext(clientKp KeyPair, serverPk []byte, info []byte, psk *Psk, seed []byte) (ClientContext, []byte, error) { 479 | dhSecret, enc, err := suite.authEncap(serverPk, clientKp, seed) 480 | if err != nil { 481 | return ClientContext{}, nil, err 482 | } 483 | mode := ModeAuth 484 | if psk != nil { 485 | mode = ModeAuthPsk 486 | } 487 | context, err := suite.keySchedule(mode, dhSecret, info, psk) 488 | if err != nil { 489 | return ClientContext{}, nil, err 490 | } 491 | return ClientContext{inner: context}, enc, nil 492 | } 493 | 494 | // NewAuthenticatedServerContext - Create a new context for a server (aka "recipient"), with authentication 495 | func (suite *Suite) NewAuthenticatedServerContext(clientPk []byte, enc []byte, serverKp KeyPair, info []byte, psk *Psk) (ServerContext, error) { 496 | dhSecret, err := suite.authDecap(enc, serverKp, clientPk) 497 | if err != nil { 498 | return ServerContext{}, err 499 | } 500 | mode := ModeAuth 501 | if psk != nil { 502 | mode = ModeAuthPsk 503 | } 504 | context, err := suite.keySchedule(mode, dhSecret, info, psk) 505 | if err != nil { 506 | return ServerContext{}, err 507 | } 508 | return ServerContext{inner: context}, nil 509 | } 510 | 511 | // NewRawCipher - Access the raw cipher interface 512 | func (suite *Suite) NewRawCipher(key []byte) (cipher.AEAD, error) { 513 | switch suite.AeadID { 514 | case AeadAes128Gcm, AeadAes256Gcm: 515 | block, err := aes.NewCipher(key) 516 | if err != nil { 517 | return nil, err 518 | } 519 | return cipher.NewGCM(block) 520 | case AeadChaCha20Poly1305: 521 | return chacha20poly1305.New(key) 522 | default: 523 | return nil, errors.New("externally defined cipher") 524 | } 525 | } 526 | 527 | func (state *aeadState) incrementCounter() error { 528 | carry := uint16(1) 529 | for i := len(state.counter); ; { 530 | i-- 531 | x := uint16(state.counter[i]) + carry 532 | state.counter[i] = byte(x & 0xff) 533 | carry = x >> 8 534 | if i == 0 { 535 | break 536 | } 537 | } 538 | if carry != 0 { 539 | return errors.New("Overflow") 540 | } 541 | return nil 542 | } 543 | 544 | // NextNonce - Get the next nonce to encrypt/decrypt a message with an AEAD 545 | // Note: this is not thread-safe. 546 | func (state *aeadState) NextNonce() []byte { 547 | if len(state.counter) != len(state.baseNonce) { 548 | panic("Inconsistent nonce length") 549 | } 550 | nonce := append(state.baseNonce[:0:0], state.baseNonce...) 551 | for i := 0; i < len(nonce); i++ { 552 | nonce[i] ^= state.counter[i] 553 | } 554 | state.incrementCounter() 555 | return nonce 556 | } 557 | 558 | // EncryptToServer - Encrypt and authenticate a message for the server, with optional associated data 559 | func (context *ClientContext) EncryptToServer(message []byte, ad []byte) ([]byte, error) { 560 | state := context.inner.outboundState 561 | nonce := state.NextNonce() 562 | return state.aead.internal().Seal(nil, nonce, message, ad), nil 563 | } 564 | 565 | // DecryptFromClient - Verify and decrypt a ciphertext received from the client, with optional associated data 566 | func (context *ServerContext) DecryptFromClient(ciphertext []byte, ad []byte) ([]byte, error) { 567 | state := context.inner.outboundState 568 | nonce := state.NextNonce() 569 | return state.aead.internal().Open(nil, nonce, ciphertext, ad) 570 | } 571 | 572 | func (inner *innerContext) responseState() (*aeadState, error) { 573 | key, err := inner.export([]byte("response key"), inner.suite.KeyBytes) 574 | if err != nil { 575 | return nil, err 576 | } 577 | baseNonce, err := inner.export([]byte("response nonce"), inner.suite.NonceBytes) 578 | if err != nil { 579 | return nil, err 580 | } 581 | return inner.suite.newAeadState(key, baseNonce) 582 | } 583 | 584 | // EncryptToClient - Encrypt and authenticate a message for the client, with optional associated data 585 | func (context *ServerContext) EncryptToClient(message []byte, ad []byte) ([]byte, error) { 586 | if context.inner.inboundState == nil { 587 | var err error 588 | context.inner.inboundState, err = context.inner.responseState() 589 | if err != nil { 590 | return nil, err 591 | } 592 | } 593 | state := context.inner.inboundState 594 | nonce := state.NextNonce() 595 | return state.aead.internal().Seal(nil, nonce, message, ad), nil 596 | } 597 | 598 | // DecryptFromServer - Verify and decrypt a ciphertext received from the server, with optional associated data 599 | func (context *ClientContext) DecryptFromServer(ciphertext []byte, ad []byte) ([]byte, error) { 600 | if context.inner.inboundState == nil { 601 | var err error 602 | context.inner.inboundState, err = context.inner.responseState() 603 | if err != nil { 604 | return nil, err 605 | } 606 | } 607 | state := context.inner.inboundState 608 | nonce := state.NextNonce() 609 | return state.aead.internal().Open(nil, nonce, ciphertext, ad) 610 | } 611 | 612 | // ExporterSecret - Return the exporter secret 613 | func (context *ClientContext) ExporterSecret() []byte { 614 | return context.inner.exporterSecret 615 | } 616 | 617 | // ExporterSecret - Return the exporter secret 618 | func (context *ServerContext) ExporterSecret() []byte { 619 | return context.inner.exporterSecret 620 | } 621 | 622 | // Export - Derive an arbitrary-long secret 623 | func (context *ClientContext) Export(exporterContext []byte, length uint16) ([]byte, error) { 624 | return context.inner.export(exporterContext, length) 625 | } 626 | 627 | // Export - Derive an arbitrary-long secret 628 | func (context *ServerContext) Export(exporterContext []byte, length uint16) ([]byte, error) { 629 | return context.inner.export(exporterContext, length) 630 | } 631 | 632 | type aeadImpl interface { 633 | internal() cipher.AEAD 634 | } 635 | 636 | type aeadAesImpl struct { 637 | impl cipher.AEAD 638 | } 639 | 640 | func newAesAead(key []byte) (aeadAesImpl, error) { 641 | block, err := aes.NewCipher(key) 642 | if err != nil { 643 | return aeadAesImpl{}, err 644 | } 645 | aesGcm, err := cipher.NewGCM(block) 646 | if err != nil { 647 | return aeadAesImpl{}, err 648 | } 649 | aead := aeadAesImpl{impl: aesGcm} 650 | return aead, nil 651 | } 652 | 653 | func (aead aeadAesImpl) internal() cipher.AEAD { 654 | return aead.impl 655 | } 656 | 657 | type aeadChaChaPolyImpl struct { 658 | impl cipher.AEAD 659 | } 660 | 661 | func newChaChaPolyAead(key []byte) (aeadChaChaPolyImpl, error) { 662 | impl, err := chacha20poly1305.New(key) 663 | if err != nil { 664 | return aeadChaChaPolyImpl{}, err 665 | } 666 | aead := aeadChaChaPolyImpl{impl: impl} 667 | return aead, nil 668 | } 669 | 670 | func (aead aeadChaChaPolyImpl) internal() cipher.AEAD { 671 | return aead.impl 672 | } 673 | -------------------------------------------------------------------------------- /hpke_test.go: -------------------------------------------------------------------------------- 1 | package hpkecompact 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/powerman/check" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | check.TestMain(m) 12 | } 13 | 14 | func TestExchange(t *testing.T) { 15 | suite, err := NewSuite(KemX25519HkdfSha256, KdfHkdfSha256, AeadAes128Gcm) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | serverKp, err := suite.GenerateKeyPair() 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | clientCtx, encryptedSharedSecret, err := suite.NewClientContext(serverKp.PublicKey, []byte("test"), nil) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | serverCtx, err := suite.NewServerContext(encryptedSharedSecret, serverKp, []byte("test"), nil) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | ciphertext, err := clientCtx.EncryptToServer([]byte("message"), nil) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | decrypted, err := serverCtx.DecryptFromClient(ciphertext, nil) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | if string(decrypted) != "message" { 46 | t.Fatal("Unexpected decryption result") 47 | } 48 | 49 | ciphertext, err = serverCtx.EncryptToClient([]byte("response"), nil) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | decrypted, err = clientCtx.DecryptFromServer(ciphertext, nil) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | if string(decrypted) != "response" { 60 | t.Fatal("Unexpected decryption result") 61 | } 62 | } 63 | 64 | func TestAuthenticatedExchange(t *testing.T) { 65 | suite, err := NewSuite(KemX25519HkdfSha256, KdfHkdfSha256, AeadChaCha20Poly1305) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | clientKp, err := suite.GenerateKeyPair() 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | serverKp, err := suite.GenerateKeyPair() 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | psk := &Psk{ID: []byte("PSK ID"), Key: []byte("PSK key")} 81 | 82 | clientCtx, encryptedSharedSecret, err := suite.NewAuthenticatedClientContext(clientKp, serverKp.PublicKey, []byte("test"), psk) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | serverCtx, err := suite.NewAuthenticatedServerContext(clientKp.PublicKey, encryptedSharedSecret, serverKp, []byte("test"), psk) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | ciphertext, err := clientCtx.EncryptToServer([]byte("message"), nil) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | decrypted, err := serverCtx.DecryptFromClient(ciphertext, nil) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | if string(decrypted) != "message" { 103 | t.Fatal("Unexpected decryption result") 104 | } 105 | } 106 | 107 | func TestVectors(t *testing.T) { 108 | ctx, err := NewSuite(KemX25519HkdfSha256, KdfHkdfSha256, AeadAes128Gcm) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | info, _ := hex.DecodeString("4f6465206f6e2061204772656369616e2055726e") 114 | 115 | serverSeed, _ := hex.DecodeString("29e5fcb544130784b7606e3160d736309d63e044c241d4461a9c9d2e9362f1db") 116 | serverKp, err := ctx.DeterministicKeyPair(serverSeed) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | if !hexEqual(serverKp.SecretKey, "ad5e716159a11fdb33527ce98fe39f24ae3449ffb6e93e8911f62c0e9781718a") { 121 | t.Fatal("Unexpected serverSk") 122 | } 123 | if !hexEqual(serverKp.PublicKey, "46570dfa9f66e17c38e7a081c65cf42bc00e6fed969d326c692748ae866eac6f") { 124 | t.Fatal("Unexpected serverPk") 125 | } 126 | 127 | clientSeed, _ := hex.DecodeString("3b8ed55f38545e6ea459b6838280b61ff4f5df2a140823373380609fb6c68933") 128 | clientCtx, encryptedSharedSecret, err := ctx.NewClientDeterministicContext(serverKp.PublicKey, info, nil, clientSeed) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | if !hexEqual(encryptedSharedSecret, "e7d9aa41faa0481c005d1343b26939c0748a5f6bf1f81fbd1a4e924bf0719149") { 133 | t.Fatal("Unexpected shared secret") 134 | } 135 | 136 | c1, _ := clientCtx.EncryptToServer([]byte("message"), []byte("ad")) 137 | if !hexEqual(c1, "dc54a1124854e041089e52066349a238380aaf6bf98a4c") { 138 | t.Fatal("Unexpected ciphertext") 139 | } 140 | 141 | c2, _ := clientCtx.EncryptToServer([]byte("message"), []byte("ad")) 142 | if !hexEqual(c2, "37fbdf5f21e77f15291212fe94579054f56eaf5e78f2b5") { 143 | t.Fatal("Unexpected second ciphertext") 144 | } 145 | 146 | if !hexEqual(clientCtx.inner.outboundState.baseNonce, "ede5198c19b2591389fc7cea") { 147 | t.Fatal("Unexpected base nonce") 148 | } 149 | 150 | es := clientCtx.ExporterSecret() 151 | if !hexEqual(es, "d27ca8c6ce9d8998f3692613c29e5ae0b064234b874a52d65a014eeffed429b9") { 152 | t.Fatal("Unexpected exported secret") 153 | } 154 | } 155 | 156 | func TestExportOnly(t *testing.T) { 157 | suite, err := NewSuite(KemX25519HkdfSha256, KdfHkdfSha256, AeadExportOnly) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | 162 | serverKp, err := suite.GenerateKeyPair() 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | 167 | clientCtx, encryptedSharedSecret, err := suite.NewClientContext(serverKp.PublicKey, []byte("test"), nil) 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | 172 | serverCtx, err := suite.NewServerContext(encryptedSharedSecret, serverKp, []byte("test"), nil) 173 | if err != nil { 174 | t.Fatal(err) 175 | } 176 | 177 | es := serverCtx.ExporterSecret() 178 | for i, x := range clientCtx.ExporterSecret() { 179 | if es[i] != x { 180 | t.Fatal("Exported secret mismatch") 181 | } 182 | } 183 | } 184 | 185 | func hexEqual(a []byte, bHex string) bool { 186 | b, _ := hex.DecodeString(bHex) 187 | if len(a) != len(b) { 188 | return false 189 | } 190 | for i, v := range a { 191 | if v != b[i] { 192 | return false 193 | } 194 | } 195 | return true 196 | } 197 | --------------------------------------------------------------------------------