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 ¶
- Variables
- func CoAPMessage1(msg []byte) []byte
- func CoAPRequest(cR, msg []byte) []byte
- func ErrorMessage(err error) []byte
- func ParseCoAPRequest(payload []byte) (cR, msg []byte, message1 bool, err error)
- type Config
- type Credential
- type DHKey
- type EAD
- type ErrCode
- type Error
- type IDCred
- type Initiator
- func (i *Initiator) ReadMessage2(ctx context.Context, msg []byte) ([]EAD, error)
- func (i *Initiator) ReadMessage4(msg []byte) ([]EAD, error)
- func (i *Initiator) Session() (*Session, error)
- func (i *Initiator) WriteMessage1(ead ...EAD) ([]byte, error)
- func (i *Initiator) WriteMessage3(ead ...EAD) ([]byte, error)
- type Message1Info
- type Method
- type OSCOREParams
- type PeerError
- type PeerInfo
- type Responder
- func (r *Responder) ReadMessage1(ctx context.Context, msg []byte) (*Message1Info, error)
- func (r *Responder) ReadMessage3(ctx context.Context, msg []byte) ([]EAD, error)
- func (r *Responder) Session() (*Session, error)
- func (r *Responder) WriteMessage2(connID []byte, ead ...EAD) ([]byte, error)
- func (r *Responder) WriteMessage4(ead ...EAD) ([]byte, error)
- type Session
- func (s *Session) Close()
- func (s *Session) ConnIDs() (cI, cR []byte)
- func (s *Session) Exporter(label int, context []byte, length int) ([]byte, error)
- func (s *Session) KeyUpdate(context []byte) error
- func (s *Session) OSCORE() (*OSCOREParams, error)
- func (s *Session) PeerCredential() Credential
- func (s *Session) Suite() Suite
- type Suite
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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.
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
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
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 ¶
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
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 ¶
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 ¶
A DHKey is a static Diffie-Hellman private key. *ecdh.PrivateKey satisfies it; so can a key held in hardware.
type EAD ¶
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.
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.
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 ¶
KCCS returns the ID_CRED { 14 : ccs } that carries an encoded CWT Claims Set by value (RFC 9528, Section 3.5.3).
func ParseIDCred ¶
ParseIDCred returns the IDCred for an encoded COSE header map, for ID_CRED forms this package has no constructor for.
func X5Chain ¶
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 ¶
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) KCCS ¶
KCCS returns the encoded CWT Claims Set if the header map is exactly { 14 : ccs }. The result can be passed to ParseCCS.
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 ¶
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 ¶
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 ¶
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) WriteMessage1 ¶
WriteMessage1 returns message_1 (RFC 9528, Section 5.2.2), carrying ead as EAD_1.
func (*Initiator) WriteMessage3 ¶
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.
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.
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 ¶
NewResponder returns a Responder for a new session. Its connection identifier is chosen later, in WriteMessage2.
func (*Responder) ReadMessage1 ¶
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 ¶
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) WriteMessage2 ¶
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 ¶
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) Exporter ¶
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 ¶
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.
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.
Source Files
¶
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. |