Help us shape the future of Go! Take the 2026 Go Developer Survey

edhoc

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

edhoc

OpenSSF Best Practices

A Go implementation of EDHOC (RFC 9528), the lightweight authenticated key exchange used to set up OSCORE contexts on constrained devices. The package turns EDHOC messages into bytes and back; carrying them, over CoAP or anything else, is left to the application.

Status

All of RFC 9528 is implemented except cipher suite 25, which needs X448 and Ed448, neither of which Go has. Both RFC 9529 traces are reproduced exactly, and handshakes with lakers 0.6.2 work in both roles. The API may change before v1.0.

Supported:

  • Authentication methods 0–3, with signature or static DH keys on either side
  • Cipher suites 0–6 and 24
  • Raw public keys (CCS) by kid and X.509 certificates by x5t, or either by value in kccs or x5chain; other ID_CRED forms through ParseIDCred
  • message_4, EAD, error messages and cipher suite negotiation
  • EDHOC_Exporter, EDHOC_KeyUpdate and OSCORE parameters
  • CoAP payloads (RFC 9528, Appendix A.2)
  • A Responder Config chosen per session with Config.GetConfig

The code is pure Go with no cgo or unsafe, and depends only on the standard library and golang.org/x/crypto. Names and comments follow RFC 9528 so the two can be read side by side.

Example

See example_test.go for complete handshakes, a CoAP server, cipher suite negotiation and X.509 certificates. In short, with error handling left out:

cfg := &edhoc.Config{
	Method:         edhoc.MethodStaticStatic,
	Suites:         []edhoc.Suite{edhoc.Suite2},
	Credential:     myCredential,
	Key:            myKey,
	PeerCredential: lookUpPeer, // func(context.Context, *edhoc.PeerInfo) (edhoc.Credential, error)
}

// Initiator
initiator, err := edhoc.NewInitiator(cfg, []byte{0x37}) // C_I for this session
msg1, err := initiator.WriteMessage1()
// send msg1, receive msg2
ead2, err := initiator.ReadMessage2(ctx, msg2)
msg3, err := initiator.WriteMessage3()
// send msg3, receive msg4
ead4, err := initiator.ReadMessage4(msg4)
session, err := initiator.Session()
oscore, err := session.OSCORE()

// Responder
responder, err := edhoc.NewResponder(cfg)
m1, err := responder.ReadMessage1(ctx, msg1)
msg2, err := responder.WriteMessage2(cR) // C_R, different from m1.PeerConnID
// send msg2, receive msg3
ead3, err := responder.ReadMessage3(ctx, msg3)
msg4, err := responder.WriteMessage4()
// send msg4
session, err := responder.Session()

When a Read method fails, send edhoc.ErrorMessage(err) to the peer, if it is not nil. Without message_4, the Initiator should not use the session's keys until it has received a message protected with them (RFC 9528, Section 5.4.2).

Go versions

Go 1.25 or later. CI tests 1.25 and the two latest releases.

Security

See SECURITY.md to report a vulnerability.

License

Copyright 2026 Last Lock Inc. Apache License 2.0; see LICENSE and NOTICE.

Documentation

Overview

Package edhoc implements EDHOC, the Ephemeral Diffie-Hellman Over COSE key exchange defined in RFC 9528.

EDHOC runs between an Initiator and a Responder:

Initiator                         Responder
WriteMessage1  --- message_1 -->  ReadMessage1
ReadMessage2   <-- message_2 ---  WriteMessage2
WriteMessage3  --- message_3 -->  ReadMessage3
ReadMessage4   <-- message_4 ---  WriteMessage4   (optional)

The package only turns messages into bytes and back; carrying them is up to the application. For CoAP (RFC 9528, Appendix A.2), CoAPMessage1 and CoAPRequest build request payloads, and ParseCoAPRequest gives a server the C_R that tells it which Responder a message_3 is for. Each Read method returns the EAD items of the message. When a Read method fails, send the peer the bytes from ErrorMessage, if any, and drop the session.

After message_3, both sides have a Session, from which application keys are derived with Session.Exporter, or the parameters of an OSCORE context with Session.OSCORE. Session.Close erases its keys.

Credentials

Each side has a Credential (CRED_I or CRED_R), identified on the wire by an IDCred (ID_CRED_I or ID_CRED_R). The library calls Config.PeerCredential to turn the peer's IDCred into its Credential, then checks that the peer holds the private key. Deciding whether that credential is one the application trusts for this connection is up to the application (RFC 9528, Appendix D). A Responder can choose its own credential, key and method once it has seen message_1 with Config.GetConfig. That matters most when the peer sends its credential by value, in an x5chain (IDCred.X5Chain) or kccs (IDCred.KCCS): anyone can send a credential, so PeerCredential must check it against a trust anchor or a list of known keys before returning it.

Status

The package implements all four authentication methods with cipher suites 0 to 6 and 24. Credentials can be raw public keys in a CWT Claims Set (NewCCS, ParseCCS) or X.509 certificates (NewX509); any other form can be used by filling in a Credential directly. The API may change before v1.0.

Example
package main

import (
	"bytes"
	"context"
	"crypto/ecdh"
	"crypto/rand"
	"fmt"
	"log"

	"github.com/aquilliam/edhoc"
)

// rawPublicKey returns a static DH key and a raw public key credential for
// it, identified by kid.
func rawPublicKey(kid byte) (*ecdh.PrivateKey, edhoc.Credential) {
	key, err := ecdh.P256().GenerateKey(rand.Reader)
	if err != nil {
		log.Fatal(err)
	}
	cred, err := edhoc.NewCCS("", []byte{kid}, key.PublicKey())
	if err != nil {
		log.Fatal(err)
	}
	return key, cred
}

func main() {
	keyI, credI := rawPublicKey(0x01)
	keyR, credR := rawPublicKey(0x02)

	// Each side knows the other's credential in advance and looks it up by
	// the ID_CRED it is sent.
	known := map[edhoc.IDCred]edhoc.Credential{credI.ID: credI, credR.ID: credR}
	lookUp := func(_ context.Context, p *edhoc.PeerInfo) (edhoc.Credential, error) {
		c, ok := known[p.ID]
		if !ok {
			return edhoc.Credential{}, edhoc.ErrUnknownCredential
		}
		return c, nil
	}

	// A Config can be shared by all sessions on one side. The connection
	// identifier is chosen per session.
	initiator, err := edhoc.NewInitiator(&edhoc.Config{
		Method:         edhoc.MethodStaticStatic,
		Suites:         []edhoc.Suite{edhoc.Suite2},
		Credential:     credI,
		Key:            keyI,
		PeerCredential: lookUp,
	}, []byte{0x37})
	if err != nil {
		log.Fatal(err)
	}
	responder, err := edhoc.NewResponder(&edhoc.Config{
		Method:         edhoc.MethodStaticStatic,
		Suites:         []edhoc.Suite{edhoc.Suite2},
		Credential:     credR,
		Key:            keyR,
		PeerCredential: lookUp,
	})
	if err != nil {
		log.Fatal(err)
	}

	// In a real application each message travels over the network, and a
	// failure on either side is answered with edhoc.ErrorMessage(err).
	msg1, err := initiator.WriteMessage1()
	if err != nil {
		log.Fatal(err)
	}
	if _, err := responder.ReadMessage1(context.Background(), msg1); err != nil {
		log.Fatal(err)
	}
	msg2, err := responder.WriteMessage2([]byte{0x27}) // C_R for this session
	if err != nil {
		log.Fatal(err)
	}
	if _, err := initiator.ReadMessage2(context.Background(), msg2); err != nil {
		log.Fatal(err)
	}
	msg3, err := initiator.WriteMessage3()
	if err != nil {
		log.Fatal(err)
	}
	if _, err := responder.ReadMessage3(context.Background(), msg3); err != nil {
		log.Fatal(err)
	}

	si, _ := initiator.Session()
	sr, _ := responder.Session()
	client, _ := si.OSCORE()
	server, _ := sr.OSCORE()
	fmt.Println("same master secret:", bytes.Equal(client.MasterSecret, server.MasterSecret))
	fmt.Printf("client sender ID %x, server sender ID %x\n", client.SenderID, server.SenderID)
}
Output:
same master secret: true
client sender ID 27, server sender ID 37
Example (Server)

A CoAP server for the EDHOC resource (RFC 9528, Appendix A.2) keeps a Responder per session between message_1 and message_3 and finds it again by the C_R in front of message_3.

package main

import (
	"bytes"
	"context"
	"crypto/ecdh"
	"crypto/rand"
	"errors"
	"fmt"
	"log"

	"github.com/aquilliam/edhoc"
)

// rawPublicKey returns a static DH key and a raw public key credential for
// it, identified by kid.
func rawPublicKey(kid byte) (*ecdh.PrivateKey, edhoc.Credential) {
	key, err := ecdh.P256().GenerateKey(rand.Reader)
	if err != nil {
		log.Fatal(err)
	}
	cred, err := edhoc.NewCCS("", []byte{kid}, key.PublicKey())
	if err != nil {
		log.Fatal(err)
	}
	return key, cred
}

func main() {
	keyI, credI := rawPublicKey(0x01)
	keyR, credR := rawPublicKey(0x02)
	cfgR := &edhoc.Config{
		Method:     edhoc.MethodStaticStatic,
		Suites:     []edhoc.Suite{edhoc.Suite2},
		Credential: credR,
		Key:        keyR,
		PeerCredential: func(_ context.Context, p *edhoc.PeerInfo) (edhoc.Credential, error) {
			if p.ID != credI.ID {
				return edhoc.Credential{}, edhoc.ErrUnknownCredential
			}
			return credI, nil
		},
	}

	// A real server guards these with a mutex, limits how many sessions are
	// pending (RFC 9528, Section 9.7) and drops them after a timeout.
	pending := map[string]*edhoc.Responder{}
	nextCR := byte(0)

	// newConnID returns a C_R that differs from C_I and from the C_R of
	// every pending session. Once a session is set up, its C_R is also its
	// OSCORE Recipient ID and must stay unique among the OSCORE contexts.
	newConnID := func(cI []byte) []byte {
		for {
			cR := []byte{nextCR}
			nextCR++
			if _, ok := pending[string(cR)]; !ok && !bytes.Equal(cR, cI) {
				return cR
			}
		}
	}

	// handle serves a POST to /.well-known/edhoc and returns the response
	// payload. When a Read method fails, the payload is the error message,
	// sent with response code 4.00 or 5.00.
	handle := func(ctx context.Context, payload []byte) ([]byte, error) {
		cR, msg, message1, err := edhoc.ParseCoAPRequest(payload)
		if err != nil {
			return nil, err
		}
		if message1 {
			r, err := edhoc.NewResponder(cfgR)
			if err != nil {
				return nil, err
			}
			m1, err := r.ReadMessage1(ctx, msg)
			if err != nil {
				return edhoc.ErrorMessage(err), err
			}
			cR := newConnID(m1.PeerConnID)
			msg2, err := r.WriteMessage2(cR)
			if err != nil {
				return nil, err
			}
			pending[string(cR)] = r
			return msg2, nil
		}
		r, ok := pending[string(cR)]
		if !ok {
			return nil, errors.New("no session for this C_R")
		}
		delete(pending, string(cR))
		if _, err := r.ReadMessage3(ctx, msg); err != nil {
			return edhoc.ErrorMessage(err), err
		}
		s, _ := r.Session()
		oscore, _ := s.OSCORE()
		fmt.Printf("server: new OSCORE context, sender ID %x, recipient ID %x\n", oscore.SenderID, oscore.RecipientID)
		return r.WriteMessage4()
	}

	// The client.
	ctx := context.Background()
	i, err := edhoc.NewInitiator(&edhoc.Config{
		Method:     edhoc.MethodStaticStatic,
		Suites:     []edhoc.Suite{edhoc.Suite2},
		Credential: credI,
		Key:        keyI,
		PeerCredential: func(_ context.Context, p *edhoc.PeerInfo) (edhoc.Credential, error) {
			if p.ID != credR.ID {
				return edhoc.Credential{}, edhoc.ErrUnknownCredential
			}
			return credR, nil
		},
	}, []byte{0x37})
	if err != nil {
		log.Fatal(err)
	}
	msg1, _ := i.WriteMessage1()
	msg2, err := handle(ctx, edhoc.CoAPMessage1(msg1))
	if err != nil {
		log.Fatal(err)
	}
	if _, err := i.ReadMessage2(ctx, msg2); err != nil {
		log.Fatal(err)
	}
	msg3, _ := i.WriteMessage3()
	s, _ := i.Session()
	_, cR := s.ConnIDs()
	msg4, err := handle(ctx, edhoc.CoAPRequest(cR, msg3))
	if err != nil {
		log.Fatal(err)
	}
	if _, err := i.ReadMessage4(msg4); err != nil {
		log.Fatal(err)
	}
	fmt.Println("client: message_4 received")
}
Output:
server: new OSCORE context, sender ID 37, recipient ID 00
client: message_4 received

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrUnknownCredential = errors.New("edhoc: unknown credential")

ErrUnknownCredential is returned by a Config.PeerCredential function when the peer refers to a credential this side does not have.

View Source
var ErrWrongState = errors.New("edhoc: method called in the wrong state")

ErrWrongState is returned when a method of an Initiator or Responder is called out of order or after the session has failed, and when a Session is used after Close.

Functions

func CoAPMessage1 added in v0.2.0

func CoAPMessage1(msg []byte) []byte

CoAPMessage1 returns the payload of the CoAP request that carries message_1 to the EDHOC resource: msg prefixed with the CBOR value true (RFC 9528, Appendix A.2).

func CoAPRequest added in v0.2.0

func CoAPRequest(cR, msg []byte) []byte

CoAPRequest returns the payload of a CoAP request that carries message_3 or an error message to the EDHOC resource: msg prefixed with C_R, the Responder's connection identifier (RFC 9528, Appendix A.2).

func ErrorMessage

func ErrorMessage(err error) []byte

ErrorMessage returns the EDHOC error message to send to the peer after err, or nil if no error message should be sent. Only errors of type *Error get a reply; misuse of the API and errors received from the peer do not.

func ParseCoAPRequest added in v0.2.0

func ParseCoAPRequest(payload []byte) (cR, msg []byte, message1 bool, err error)

ParseCoAPRequest splits the payload of a CoAP request to the EDHOC resource. For message_1, message1 is true and cR is nil. Otherwise cR is the connection identifier that selects the Responder msg is for. cR and msg share memory with payload.

Types

type Config

type Config struct {
	// Method is the authentication method. Both sides must use the same one.
	Method Method

	// Suites lists the supported cipher suites. For the Initiator the order
	// is its order of preference, most preferred first. For a Responder with
	// GetConfig, it lists every suite the Responder supports.
	Suites []Suite

	// Credential is this side's authentication credential, CRED_I or CRED_R.
	Credential Credential

	// Key is the private authentication key matching Credential: a
	// crypto.Signer if this side authenticates with a signature, a DHKey if
	// it uses static DH. Signing keys are ed25519.PrivateKey for EdDSA and
	// *ecdsa.PrivateKey on P-256 for ES256 or on P-384 for ES384, or anything
	// else that implements crypto.Signer with those public keys, such as a key
	// in a KMS. Static DH keys must be on the curve of the suite. An
	// Initiator's key must suit the cipher suite it selects, and a
	// Responder's every suite in Suites; a Responder that needs different
	// keys for different suites uses GetConfig.
	Key crypto.PrivateKey

	// PeerCredential returns the peer's credential for the ID_CRED field it
	// sent, or an error wrapping ErrUnknownCredential if there is no such
	// credential. It is called during ReadMessage2 on the Initiator and
	// ReadMessage3 on the Responder, with the context passed to them, before
	// the peer has been authenticated, so nothing in p is proof of anything.
	// The returned credential is the one the peer must then prove it holds
	// the key for.
	PeerCredential func(ctx context.Context, p *PeerInfo) (Credential, error)

	// EADLabels lists the absolute values of the EAD labels the application
	// handles. A received critical EAD item with any other label fails the
	// session. Padding (label 0) is always discarded.
	EADLabels []int

	// GetConfig, if set, is called by a Responder during ReadMessage1, with
	// the context passed to it, once the selected cipher suite has been
	// accepted. It returns the Config for the rest of the session, which
	// must have the method m.Method and include m.Suite. It lets one
	// Responder accept several methods, or choose its credential by the
	// selected suite or by EAD_1. When GetConfig is set, the Responder uses
	// only Suites from this Config, for cipher suite negotiation, and ignores
	// the GetConfig field of the returned Config. An Initiator ignores
	// GetConfig.
	GetConfig func(ctx context.Context, m *Message1Info) (*Config, error)
}

Config configures one side of EDHOC sessions. A Config may be shared by any number of sessions but must not be changed once it has been passed to NewInitiator or NewResponder. Values that differ between sessions, such as the connection identifier, are passed to NewInitiator and NewResponder.

type Credential

type Credential struct {
	// ID identifies the credential in EDHOC messages.
	ID IDCred

	// Raw is the credential exactly as both sides use it in the transcript,
	// for example the encoded CWT Claims Set of a raw public key.
	Raw []byte

	// PublicKey is the authentication key in the credential: an
	// ed25519.PublicKey for EdDSA, an *ecdh.PublicKey for X25519, and either
	// an *ecdsa.PublicKey or an *ecdh.PublicKey on P-256 or P-384.
	PublicKey crypto.PublicKey
}

A Credential is an authentication credential, CRED_I or CRED_R (RFC 9528, Section 3.5.2).

func NewCCS

func NewCCS(subject string, kid []byte, pub crypto.PublicKey) (Credential, error)

NewCCS returns a credential for a raw public key: a CWT Claims Set with an optional subject claim and a COSE_Key in a 'cnf' claim (RFC 9528, Section 3.5.2). The credential's ID is { 4 : kid }. pub may be a P-256 or P-384 key (*ecdh.PublicKey or *ecdsa.PublicKey), an X25519 key (*ecdh.PublicKey) or an ed25519.PublicKey.

func NewX509

func NewX509(der []byte) (Credential, error)

NewX509 returns the credential for a DER-encoded X.509 certificate, identified by X5T. To send the certificate by value instead, set ID to X5Chain(der, intermediates...). CRED is the certificate as a CBOR byte string (RFC 9528, Section 3.5.2). The certificate is parsed for its public key but not verified; that is up to the application.

Example

Both sides send their X.509 certificate by value and check the peer's against a shared root in PeerCredential.

package main

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"fmt"
	"log"
	"math/big"
	"time"

	"github.com/aquilliam/edhoc"
)

func main() {
	ctx := context.Background()
	caKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	caTmpl := &x509.Certificate{
		SerialNumber:          big.NewInt(1),
		Subject:               pkix.Name{CommonName: "root"},
		NotBefore:             time.Now().Add(-time.Hour),
		NotAfter:              time.Now().Add(time.Hour),
		IsCA:                  true,
		BasicConstraintsValid: true,
		KeyUsage:              x509.KeyUsageCertSign,
	}
	caDER, _ := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey)
	ca, _ := x509.ParseCertificate(caDER)
	roots := x509.NewCertPool()
	roots.AddCert(ca)

	// party returns a signing key and a credential with a certificate for it,
	// sent by value.
	party := func(name string, serial int64) (*ecdsa.PrivateKey, edhoc.Credential) {
		key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
		tmpl := &x509.Certificate{
			SerialNumber: big.NewInt(serial),
			Subject:      pkix.Name{CommonName: name},
			NotBefore:    time.Now().Add(-time.Hour),
			NotAfter:     time.Now().Add(time.Hour),
			KeyUsage:     x509.KeyUsageDigitalSignature,
		}
		der, err := x509.CreateCertificate(rand.Reader, tmpl, ca, &key.PublicKey, caKey)
		if err != nil {
			log.Fatal(err)
		}
		cred, err := edhoc.NewX509(der)
		if err != nil {
			log.Fatal(err)
		}
		cred.ID = edhoc.X5Chain(der)
		return key, cred
	}

	// verify accepts any certificate issued by the root.
	verify := func(_ context.Context, p *edhoc.PeerInfo) (edhoc.Credential, error) {
		chain, ok := p.ID.X5Chain()
		if !ok {
			return edhoc.Credential{}, edhoc.ErrUnknownCredential
		}
		leaf, err := x509.ParseCertificate(chain[0])
		if err != nil {
			return edhoc.Credential{}, err
		}
		intermediates := x509.NewCertPool()
		for _, der := range chain[1:] {
			c, err := x509.ParseCertificate(der)
			if err != nil {
				return edhoc.Credential{}, err
			}
			intermediates.AddCert(c)
		}
		if _, err := leaf.Verify(x509.VerifyOptions{
			Roots:         roots,
			Intermediates: intermediates,
			KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
		}); err != nil {
			return edhoc.Credential{}, err
		}
		fmt.Println("verified", leaf.Subject.CommonName)
		return edhoc.NewX509(chain[0])
	}

	keyI, credI := party("phone", 2)
	keyR, credR := party("lock", 3)
	i, err := edhoc.NewInitiator(&edhoc.Config{
		Method:         edhoc.MethodSignSign,
		Suites:         []edhoc.Suite{edhoc.Suite2},
		Credential:     credI,
		Key:            keyI,
		PeerCredential: verify,
	}, []byte{0x37})
	if err != nil {
		log.Fatal(err)
	}
	r, err := edhoc.NewResponder(&edhoc.Config{
		Method:         edhoc.MethodSignSign,
		Suites:         []edhoc.Suite{edhoc.Suite2},
		Credential:     credR,
		Key:            keyR,
		PeerCredential: verify,
	})
	if err != nil {
		log.Fatal(err)
	}

	msg1, _ := i.WriteMessage1()
	if _, err := r.ReadMessage1(ctx, msg1); err != nil {
		log.Fatal(err)
	}
	msg2, _ := r.WriteMessage2([]byte{0x27})
	if _, err := i.ReadMessage2(ctx, msg2); err != nil {
		log.Fatal(err)
	}
	msg3, _ := i.WriteMessage3()
	if _, err := r.ReadMessage3(ctx, msg3); err != nil {
		log.Fatal(err)
	}
}
Output:
verified lock
verified phone

func ParseCCS

func ParseCCS(ccs []byte) (Credential, error)

ParseCCS returns the credential for a CWT Claims Set holding a COSE_Key in a 'cnf' claim, the way EDHOC carries raw public keys. If the COSE_Key has a kid, the credential's ID is { 4 : kid }; otherwise the caller sets ID. To send the claims set by value, set ID to KCCS(ccs).

Supported keys are P-256 and P-384 (EC2 with both coordinates), X25519 and Ed25519. P-256, P-384 and X25519 keys come back as *ecdh.PublicKey, Ed25519 keys as ed25519.PublicKey.

type DHKey

type DHKey interface {
	PublicKey() *ecdh.PublicKey
	ECDH(remote *ecdh.PublicKey) ([]byte, error)
}

A DHKey is a static Diffie-Hellman private key. *ecdh.PrivateKey satisfies it; so can a key held in hardware.

type EAD

type EAD struct {
	Label int
	Value []byte
}

An EAD is an external authorization data item (RFC 9528, Section 3.8). A negative Label marks the item as critical. Value is nil when the item has no ead_value.

type ErrCode added in v0.2.0

type ErrCode int

An ErrCode is an ERR_CODE value of an EDHOC error message (RFC 9528, Section 6).

const (
	ErrCodeUnspecified        ErrCode = 1
	ErrCodeWrongSelectedSuite ErrCode = 2
	ErrCodeUnknownCredential  ErrCode = 3
)

The ERR_CODE values defined in RFC 9528, Section 6.

func (ErrCode) String added in v0.2.0

func (c ErrCode) String() string

type Error

type Error struct {
	// Code is the ERR_CODE to send.
	Code ErrCode
	// Info is the diagnostic text sent with ErrCodeUnspecified.
	Info string
	// Suites is SUITES_R, sent with ErrCodeWrongSelectedSuite.
	Suites []Suite
	// contains filtered or unexported fields
}

An Error is a failure found while processing an EDHOC message. The session is over; pass the error to ErrorMessage to get the error message to send to the peer.

An application that rejects a message for a reason of its own, such as an EAD item it cannot process (RFC 9528, Section 3.8), can send ErrorMessage(&Error{Code: ErrCodeUnspecified, Info: "..."}) and drop the session.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type IDCred

type IDCred struct {
	// contains filtered or unexported fields
}

An IDCred is an ID_CRED field (RFC 9528, Section 3.5.3): a COSE header map that identifies a credential, such as { 4 : kid }. Two IDCreds are equal with == when their encodings are the same, so an IDCred can be a map key.

func KCCS

func KCCS(ccs []byte) IDCred

KCCS returns the ID_CRED { 14 : ccs } that carries an encoded CWT Claims Set by value (RFC 9528, Section 3.5.3).

func KeyID

func KeyID(kid []byte) IDCred

KeyID returns the ID_CRED { 4 : kid }.

func ParseIDCred

func ParseIDCred(raw []byte) (IDCred, error)

ParseIDCred returns the IDCred for an encoded COSE header map, for ID_CRED forms this package has no constructor for.

func X5Chain

func X5Chain(certs ...[]byte) IDCred

X5Chain returns the ID_CRED { 33 : x5chain } that carries an X.509 certificate chain by value (RFC 9360). certs are DER-encoded, starting with the end-entity certificate. It panics if certs is empty.

func X5T

func X5T(der []byte) IDCred

X5T returns the ID_CRED { 34 : [-15, hash] } that identifies a DER-encoded X.509 certificate by the first 8 bytes of its SHA-256 hash (RFC 9360).

func (IDCred) Bytes

func (id IDCred) Bytes() []byte

Bytes returns the encoded header map.

func (IDCred) Equal

func (id IDCred) Equal(other IDCred) bool

Equal reports whether id and other have the same encoding.

func (IDCred) KCCS

func (id IDCred) KCCS() ([]byte, bool)

KCCS returns the encoded CWT Claims Set if the header map is exactly { 14 : ccs }. The result can be passed to ParseCCS.

func (IDCred) KeyID

func (id IDCred) KeyID() ([]byte, bool)

KeyID returns the kid if the header map is exactly { 4 : kid }.

func (IDCred) X5Chain

func (id IDCred) X5Chain() ([][]byte, bool)

X5Chain returns the DER-encoded certificates if the header map is exactly { 33 : x5chain }. The certificates are not parsed or verified.

type Initiator

type Initiator struct {
	// contains filtered or unexported fields
}

An Initiator runs the Initiator side of one EDHOC session. Call WriteMessage1, ReadMessage2, WriteMessage3 and, if the application uses it, ReadMessage4, in that order. After any error the session is over and every later call fails.

An Initiator is not safe for concurrent use.

func NewInitiator

func NewInitiator(cfg *Config, connID []byte, peerSuites ...Suite) (*Initiator, error)

NewInitiator returns an Initiator for a new session. connID is C_I, this side's connection identifier. It is also this side's OSCORE Recipient ID, so it must differ from that of any other active session using the same OSCORE endpoint.

The Initiator offers its most preferred suite from cfg.Suites. To start again after the Responder rejected that choice, pass the suites from the *PeerError as peerSuites; the Initiator then selects its most preferred suite among them. peerSuites can also be suites known in advance to be supported by the Responder.

Example (Retry)

When the Responder rejects the selected cipher suite, the Initiator starts a new session with the suites from the error.

package main

import (
	"context"
	"crypto/ecdh"
	"crypto/rand"
	"errors"
	"fmt"
	"log"

	"github.com/aquilliam/edhoc"
)

// rawPublicKey returns a static DH key and a raw public key credential for
// it, identified by kid.
func rawPublicKey(kid byte) (*ecdh.PrivateKey, edhoc.Credential) {
	key, err := ecdh.P256().GenerateKey(rand.Reader)
	if err != nil {
		log.Fatal(err)
	}
	cred, err := edhoc.NewCCS("", []byte{kid}, key.PublicKey())
	if err != nil {
		log.Fatal(err)
	}
	return key, cred
}

func main() {
	ctx := context.Background()
	keyI, credI := rawPublicKey(0x01)
	keyR, credR := rawPublicKey(0x02)
	known := func(c edhoc.Credential) func(context.Context, *edhoc.PeerInfo) (edhoc.Credential, error) {
		return func(context.Context, *edhoc.PeerInfo) (edhoc.Credential, error) { return c, nil }
	}
	cfgI := &edhoc.Config{
		Method:         edhoc.MethodStaticStatic,
		Suites:         []edhoc.Suite{edhoc.Suite3, edhoc.Suite2},
		Credential:     credI,
		Key:            keyI,
		PeerCredential: known(credR),
	}
	cfgR := &edhoc.Config{
		Method:         edhoc.MethodStaticStatic,
		Suites:         []edhoc.Suite{edhoc.Suite2},
		Credential:     credR,
		Key:            keyR,
		PeerCredential: known(credI),
	}

	i, _ := edhoc.NewInitiator(cfgI, []byte{0x37})
	msg1, _ := i.WriteMessage1()
	r, _ := edhoc.NewResponder(cfgR)
	_, err := r.ReadMessage1(ctx, msg1)
	reply := edhoc.ErrorMessage(err)

	_, err = i.ReadMessage2(ctx, reply)
	var pe *edhoc.PeerError
	if !errors.As(err, &pe) || pe.Code != edhoc.ErrCodeWrongSelectedSuite {
		log.Fatal(err)
	}
	fmt.Println("Responder supports", pe.Suites)

	i, err = edhoc.NewInitiator(cfgI, []byte{0x37}, pe.Suites...)
	if err != nil {
		log.Fatal(err)
	}
	msg1, _ = i.WriteMessage1()
	r, _ = edhoc.NewResponder(cfgR)
	_, err = r.ReadMessage1(ctx, msg1)
	fmt.Println("second message_1 accepted:", err == nil)
}
Output:
Responder supports [suite 2 (AES-CCM-16-64-128, SHA-256, 8, P-256, ES256)]
second message_1 accepted: true

func (*Initiator) ReadMessage2

func (i *Initiator) ReadMessage2(ctx context.Context, msg []byte) ([]EAD, error)

ReadMessage2 processes message_2 (RFC 9528, Section 5.3.3) and returns its EAD items, without padding. It calls Config.PeerCredential with ctx to find the Responder's credential and checks the Responder's proof of possession of the key in it. If the Responder sent an error message instead, the error is a *PeerError.

func (*Initiator) ReadMessage4

func (i *Initiator) ReadMessage4(msg []byte) ([]EAD, error)

ReadMessage4 processes message_4 (RFC 9528, Section 5.5.3) and returns its EAD items, without padding. Once it succeeds, the Initiator knows the Responder has derived the same PRK_out. If the Responder sent an error message instead, the error is a *PeerError.

func (*Initiator) Session

func (i *Initiator) Session() (*Session, error)

Session returns the session once WriteMessage3 has succeeded.

func (*Initiator) WriteMessage1

func (i *Initiator) WriteMessage1(ead ...EAD) ([]byte, error)

WriteMessage1 returns message_1 (RFC 9528, Section 5.2.2), carrying ead as EAD_1.

func (*Initiator) WriteMessage3

func (i *Initiator) WriteMessage3(ead ...EAD) ([]byte, error)

WriteMessage3 returns message_3 (RFC 9528, Section 5.4.2), carrying ead as EAD_3. Once it succeeds, Session returns the session.

type Message1Info added in v0.2.0

type Message1Info struct {
	// Method is the authentication method the Initiator chose.
	Method Method

	// Suite is the cipher suite the Initiator selected and the Responder
	// accepted, and Suites is SUITES_I, which ends with Suite.
	Suite  Suite
	Suites []Suite

	// EAD holds the EAD items in message_1, without padding.
	EAD []EAD

	// PeerConnID is C_I, the Initiator's connection identifier.
	PeerConnID []byte
}

Message1Info describes a received message_1. Responder.ReadMessage1 returns it, and Config.GetConfig is passed it.

type Method

type Method int

A Method is an EDHOC authentication method (RFC 9528, Section 3.2). It says whether each side authenticates with a signature key or a static Diffie-Hellman key.

const (
	MethodSignSign     Method = 0 // Initiator signs, Responder signs
	MethodSignStatic   Method = 1 // Initiator signs, Responder uses static DH
	MethodStaticSign   Method = 2 // Initiator uses static DH, Responder signs
	MethodStaticStatic Method = 3 // both use static DH
)

func (Method) String added in v0.2.0

func (m Method) String() string

type OSCOREParams added in v0.2.0

type OSCOREParams struct {
	MasterSecret []byte
	MasterSalt   []byte
	SenderID     []byte
	RecipientID  []byte
	// AEADAlg is the COSE identifier of the AEAD algorithm, such as 10 for
	// AES-CCM-16-64-128.
	AEADAlg int
	// HashAlg is the COSE identifier of the hash used with HKDF, such as -16
	// for SHA-256.
	HashAlg int
}

OSCOREParams holds the input parameters for an OSCORE security context (RFC 8613, Section 3.2) derived from an EDHOC session.

type PeerError

type PeerError struct {
	Code ErrCode
	// Info is the diagnostic text sent with ErrCodeUnspecified.
	Info string
	// Suites is SUITES_R, sent with ErrCodeWrongSelectedSuite. Use it as
	// the peerSuites argument of NewInitiator when starting the next session.
	Suites []Suite
}

A PeerError is an EDHOC error message received from the peer.

func (*PeerError) Error

func (e *PeerError) Error() string

type PeerInfo added in v0.2.0

type PeerInfo struct {
	// ID is the ID_CRED field the peer sent: ID_CRED_R in message_2 or
	// ID_CRED_I in message_3.
	ID IDCred

	// EAD holds the EAD items sent in the same message, without padding.
	EAD []EAD

	// ConnID is this side's connection identifier and PeerConnID the peer's.
	ConnID, PeerConnID []byte
}

PeerInfo is what Config.PeerCredential is told about the peer and the session.

type Responder

type Responder struct {
	// contains filtered or unexported fields
}

A Responder runs the Responder side of one EDHOC session. Call ReadMessage1, WriteMessage2, ReadMessage3 and, if the application uses it, WriteMessage4, in that order. After any error the session is over and every later call fails.

A Responder keeps state between message_1 and message_3. A server that accepts sessions from untrusted peers should limit how many it keeps (RFC 9528, Section 9.7).

A Responder is not safe for concurrent use.

func NewResponder

func NewResponder(cfg *Config) (*Responder, error)

NewResponder returns a Responder for a new session. Its connection identifier is chosen later, in WriteMessage2.

func (*Responder) ReadMessage1

func (r *Responder) ReadMessage1(ctx context.Context, msg []byte) (*Message1Info, error)

ReadMessage1 processes message_1 (RFC 9528, Section 5.2.3) and returns what it carries. If Config.GetConfig is set, it is called with ctx. If the Initiator selected a cipher suite this side will not use, the error is an *Error with code ErrCodeWrongSelectedSuite; send ErrorMessage(err) back so the Initiator can try again.

func (*Responder) ReadMessage3

func (r *Responder) ReadMessage3(ctx context.Context, msg []byte) ([]EAD, error)

ReadMessage3 processes message_3 (RFC 9528, Section 5.4.3). It calls Config.PeerCredential with ctx to find the Initiator's credential and checks the Initiator's proof of possession of the key in it. It returns the EAD items of message_3, without padding. Once it succeeds, Session returns the session. If the Initiator sent an error message instead, the error is a *PeerError.

func (*Responder) Session

func (r *Responder) Session() (*Session, error)

Session returns the session once ReadMessage3 has succeeded.

func (*Responder) WriteMessage2

func (r *Responder) WriteMessage2(connID []byte, ead ...EAD) ([]byte, error)

WriteMessage2 returns message_2 (RFC 9528, Section 5.3.2), carrying ead as EAD_2. connID is C_R, this side's connection identifier. It must differ from C_I, Message1Info.PeerConnID; if it does not, WriteMessage2 returns an error and can be called again with another connID. It is also this side's OSCORE Recipient ID, and over CoAP the Initiator puts it in front of message_3 so that a server can find the Responder, so it must differ from that of any other active session.

func (*Responder) WriteMessage4

func (r *Responder) WriteMessage4(ead ...EAD) ([]byte, error)

WriteMessage4 returns message_4 (RFC 9528, Section 5.5.2), carrying ead as EAD_4. message_4 is optional; send it when the Responder will not send the Initiator any application data protected with the new keys.

type Session

type Session struct {
	// contains filtered or unexported fields
}

A Session is the result of a completed EDHOC handshake. Keys for the application are derived from it with Exporter or OSCORE.

The Initiator should not store the session or its keys, or send protected application data, until it has received message_4 or a message protected with a derived key from the Responder (RFC 9528, Section 5.4.2).

Call Close once the application no longer needs to derive keys from the session. A Session is safe for concurrent use.

func (*Session) Close added in v0.2.0

func (s *Session) Close()

Close erases the session's keying material. Exporter, KeyUpdate and OSCORE then return ErrWrongState. Keys already exported are not affected.

func (*Session) ConnIDs

func (s *Session) ConnIDs() (cI, cR []byte)

ConnIDs returns the connection identifiers C_I and C_R.

func (*Session) Exporter

func (s *Session) Exporter(label int, context []byte, length int) ([]byte, error)

Exporter derives length bytes of keying material for the application (EDHOC_Exporter, RFC 9528, Section 4.2.1). The pair of label and context must not be used for two different purposes.

func (*Session) KeyUpdate

func (s *Session) KeyUpdate(context []byte) error

KeyUpdate replaces PRK_out with one derived from it and context (EDHOC_KeyUpdate, RFC 9528, Appendix H). Both sides must use the same context. Keys exported before the update stay valid until the application deletes them, which it should do as soon as they are no longer needed.

func (*Session) OSCORE

func (s *Session) OSCORE() (*OSCOREParams, error)

OSCORE returns the OSCORE parameters for this session with the default lengths from RFC 9528, Appendix A.1: a Master Secret as long as the application AEAD key and an 8-byte Master Salt. Use Exporter with labels 0 and 1 if the application has agreed on other lengths. It fails if a connection identifier is too long to be an OSCORE Sender ID: longer than the application AEAD nonce minus 6 bytes (RFC 8613, Section 3.3), which is 7 bytes for AES-CCM and 6 for the other algorithms.

func (*Session) PeerCredential

func (s *Session) PeerCredential() Credential

PeerCredential returns the credential the peer authenticated with.

func (*Session) Suite

func (s *Session) Suite() Suite

Suite returns the selected cipher suite.

type Suite

type Suite int

A Suite is an EDHOC cipher suite (RFC 9528, Section 3.6).

const (
	Suite0  Suite = 0  // AES-CCM-16-64-128, SHA-256, 8, X25519, EdDSA
	Suite1  Suite = 1  // AES-CCM-16-128-128, SHA-256, 16, X25519, EdDSA
	Suite2  Suite = 2  // AES-CCM-16-64-128, SHA-256, 8, P-256, ES256
	Suite3  Suite = 3  // AES-CCM-16-128-128, SHA-256, 16, P-256, ES256
	Suite4  Suite = 4  // ChaCha20/Poly1305, SHA-256, 16, X25519, EdDSA
	Suite5  Suite = 5  // ChaCha20/Poly1305, SHA-256, 16, P-256, ES256
	Suite6  Suite = 6  // A128GCM, SHA-256, 16, X25519, ES256
	Suite24 Suite = 24 // A256GCM, SHA-384, 16, P-384, ES384
)

The cipher suites this package implements, from RFC 9528, Section 10.2. The comment after each lists the EDHOC AEAD and hash algorithms, the MAC length for static DH authentication, and the key exchange and signature algorithms. Suites 0 to 3 use AES-CCM-16-64-128 and SHA-256 for the application; the others use their own EDHOC AEAD and hash.

func (Suite) String added in v0.2.0

func (s Suite) String() string

String returns the suite's label and, for a suite this package implements, its algorithms, such as "suite 2 (AES-CCM-16-64-128, SHA-256, 8, P-256, ES256)".

Directories

Path Synopsis
internal
cbor
Package cbor encodes and decodes the subset of CBOR (RFC 8949) used by EDHOC.
Package cbor encodes and decodes the subset of CBOR (RFC 8949) used by EDHOC.
ccm
Package ccm implements the CCM mode of authenticated encryption defined in NIST SP 800-38C and RFC 3610.
Package ccm implements the CCM mode of authenticated encryption defined in NIST SP 800-38C and RFC 3610.
suites
Package suites holds the EDHOC cipher suites (RFC 9528, Section 3.6) and the algorithms behind them.
Package suites holds the EDHOC cipher suites (RFC 9528, Section 3.6) and the algorithms behind them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL