Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ This includes example code for sending an LPC limit 5 seconds after connecting t
#### First Run

```sh
go run examples/controlbox/main.go 4713
go run examples/controlbox/main.go -port 4713
```

`4713` is the example server port that this process should listen on
Expand All @@ -53,12 +53,13 @@ The certificate and key and the local SKI will be generated and printed. You sho
#### General Usage

```sh
Usage: go run examples/controlbox/main.go <serverport> <remoteski> <certfile> <keyfile>
Usage: go run examples/controlbox/main.go -port <serverport> [-certpath <certfile>] [-keypath <keyfile>] [-remoteski <remoteski>] [-target <pairing target>] [-target <pairing target>...]
```

- `remoteski` is the SKI of the remote device or service you want to connect to
- `certfile` is a local file containing the generated certificate in the first usage run
- `keyfile` is a local file containing the generated key in the first usage run
- `pairing target` can be a SHIP QR Code or in this format: `SKI=...,Fingerprint=...,ShipID=...,Secret=hex`

### HEMS

Expand All @@ -67,7 +68,7 @@ This includes example code for accepting LPC and LPP limits from a control box,
#### First Run

```sh
go run examples/hems/main.go 4714
go run examples/hems/main.go -port 4714
```

`4714` is the example server port that this process should listen on
Expand All @@ -77,12 +78,13 @@ The certificate and key and the local SKI will be generated and printed. You sho
#### General Usage

```sh
Usage: go run examples/hems/main.go <serverport> <remoteski> <certfile> <keyfile>
Usage: go run examples/hems/main.go -port <serverport> [-certpath <certfile>] [-keypath <keyfile>] [-remoteski <remoteski>] [-secret <secret>]
```

- `remoteski` is the SKI of the remote device or service you want to connect to
- `certfile` is a local file containing the generated certificate in the first usage run
- `keyfile` is a local file containing the generated key in the first usage run
- `secret` is a hexadecimal secret key as specified by SHIP Pairing Service Specificiation

### EVSE

Expand All @@ -91,7 +93,7 @@ This includes example code for accepting LPC from a control box.
#### First Run

```sh
go run examples/hems/main.go 4715
go run examples/hems/main.go -port 4715
```

`4715` is the example server port that this process should listen on
Expand All @@ -101,12 +103,13 @@ The certificate and key and the local SKI will be generated and printed. You sho
#### General Usage

```sh
Usage: go run examples/evse/main.go <serverport> <remoteski> <certfile> <keyfile>
Usage: go run examples/evse/main.go -port <serverport> [-certpath <certfile>] [-keypath <keyfile>] [-remoteski <remoteski>] [-secret <secret>]
```

- `remoteski` is the SKI of the remote device or service you want to connect to
- `certfile` is a local file containing the generated certificate in the first usage run
- `keyfile` is a local file containing the generated key in the first usage run
- `secret` is a hexadecimal secret key as specified by SHIP Pairing Service Specificiation

### Explanation

Expand Down
121 changes: 91 additions & 30 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ import (
//
// implemented by service, used by the eebus service implementation
type ServiceInterface interface {
// setup the service
// Setup the service
//
// Returns error with description of the error that cannot be recovered from
Setup() error

// start the service
Start()
//
// Returns error with description of the error that cannot be recovered from
Start() error

// shutdown the service
Shutdown()
Expand All @@ -44,8 +48,11 @@ type ServiceInterface interface {

// Passthough functions to HubInterface

// Provide the current pairing state for a SKI
PairingDetailForSki(ski string) *shipapi.ConnectionStateDetail
// Provide the current pairing state for a ServiceIdentity
PairingDetailFor(identity shipapi.ServiceIdentity) *shipapi.ConnectionStateDetail

// Return the remote service details for a given ServiceIdentity
RemoteServiceFor(identity shipapi.ServiceIdentity) *shipapi.ServiceDetails

// Defines wether incoming pairing requests should be automatically accepted or not
//
Expand All @@ -55,36 +62,36 @@ type ServiceInterface interface {
// Returns if the service has auto accept enabled or not
IsAutoAcceptEnabled() bool

// Returns the QR code text for the service
// as defined in SHIP Requirements for Installation Process V1.0.0
QRCodeText() string

// Returns the Service detail of a remote SKI
RemoteServiceForSKI(ski string) *shipapi.ServiceDetails
// Generate a QR code string
//
// If a pairing config with a secret is set: generates SHIP Pairing Service QR format
// Otherwise: generates standard SHIP QR format
//
// Must be called after Setup() as it requires the hub to be initialized.
QRCodeText() (string, error)

// Pair a remote service based on the SKI
// Pair a remote service using ServiceIdentity
//
// Parameters:
// - ski: the SKI of the remote service (required)
// - shipID: the SHIP ID of the remote service (optional)
// - identity: ServiceIdentity containing SKI, fingerprint, and/or SHIP ID
//
// Note: The SHIP ID is optional, but should be provided if available.
// if provided, it will be used to validate the remote service is
// providing this SHIP ID during the handshake process and will reject
// the connection if it does not match.
RegisterRemoteSKI(ski, shipID string)
RegisterRemoteService(identity shipapi.ServiceIdentity)

// Sets the SKI as not being paired
UnregisterRemoteSKI(ski string)
// Unpair a remote service using ServiceIdentity
UnregisterRemoteService(identity shipapi.ServiceIdentity)

// Disconnect from a connected remote SKI
DisconnectSKI(ski string, reason string)
// Disconnect a connection using ServiceIdentity
DisconnectService(identity shipapi.ServiceIdentity, reason string)

// Cancels the pairing process for a SKI
// Cancels the pairing process for a ServiceIdentity
//
// This should be called while the service is running and the end
// user wants to cancel/disallow an incoming pairing request
CancelPairingWithSKI(ski string)
CancelPairing(identity shipapi.ServiceIdentity)

// Define wether the user is able to react to an incoming pairing request
//
Expand All @@ -94,6 +101,46 @@ type ServiceInterface interface {
// Default is set to false, meaning every incoming pairing request will be
// automatically denied
UserIsAbleToApproveOrCancelPairingRequests(allow bool)

// Calculate SHA-256 fingerprint of local certificate
GetLocalCertificateFingerprint() (string, error)

// **************************
// SHIP Pairing Service APIs
// **************************

// Start announcing pairing to a specific target device
// Used by devZ only.
//
// Parameters:
// - target: Pairing target
StartAnnouncementTo(target shipapi.PairingTarget) error

// Stop announcing pairing to a specific target device
// Used by devZ only.
//
// Parameters:
// - shipID: Target SHIP ID
StopAnnouncementTo(shipID string) error

// Return true if currently announcing to a specific target device
// Used by devZ only.
//
// Parameters:
// - shipID: Target SHIP ID
IsAnnouncingTo(shipID string) bool

// SHIP Pairing: Get Active Announcements.
// Used by devZ only.
//
// Returns: List of SHIP IDs currently being announced to
GetActiveAnnouncements() []string

// SHIP Pairing: Get the SHIP ID and Fingerprint of controlbox paired via SHIP Pairing
// Used by devA only.
//
// Returns: the ServiceDetails of any trusted AddCu device. Or nil if none
GetTrustedAddCuDevice() *shipapi.ServiceDetails
}

// interface for receiving data for specific events from Service
Expand All @@ -103,22 +150,36 @@ type ServiceInterface interface {
//
// implemented by the eebus service implementation, used by service
type ServiceReaderInterface interface {
// report a connection to a SKI
RemoteSKIConnected(service ServiceInterface, ski string)
// report a connection to a remote service
RemoteServiceConnected(service ServiceInterface, identity shipapi.ServiceIdentity)

// report a disconnection to a SKI
RemoteSKIDisconnected(service ServiceInterface, ski string)
// report a disconnection from a remote service
RemoteServiceDisconnected(service ServiceInterface, identity shipapi.ServiceIdentity)

// report all currently visible EEBUS services
VisibleRemoteServicesUpdated(service ServiceInterface, entries []shipapi.RemoteService)
VisibleRemoteMdnsServicesUpdated(service ServiceInterface, entries []shipapi.RemoteMdnsService)

// Provides the SHIP ID the remote service reported during the handshake process
// This needs to be persisted and passed on for future remote service connections
// when using `PairRemoteService`
ServiceShipIDUpdate(ski string, shipdID string)
// report that service information has been updated
// This includes updates to ShipID, fingerprint, or other service details
// discovered during handshake
ServiceUpdated(identity shipapi.ServiceIdentity)

// Provides the current pairing state for the remote service
// This is called whenever the state changes and can be used to
// provide user information for the pairing/connection process
ServicePairingDetailUpdate(ski string, detail *shipapi.ConnectionStateDetail)
ServicePairingDetailUpdate(identity shipapi.ServiceIdentity, detail *shipapi.ConnectionStateDetail)

// ****************************
// SHIP Pairing Service Events
// ****************************

// Called when a device is automatically trusted via SHIP pairing
ServiceAutoTrusted(service ServiceInterface, identity shipapi.ServiceIdentity)

// Called when SHIP pairing fails for a device
ServiceAutoTrustFailed(service ServiceInterface, identity shipapi.ServiceIdentity, reason error)

// Called when device trust is automatically removed
// This can happen due to device replacement timeout or new device pairing
ServiceAutoTrustRemoved(service ServiceInterface, identity shipapi.ServiceIdentity, reason string)
}
27 changes: 26 additions & 1 deletion api/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ type Configuration struct {

// Optional set which mDNS providers should be used
mdnsProviderSelection mdns.MdnsProviderSelection

// SHIP pairing configuration (optional, set before Setup)
pairingConfig *shipapi.PairingConfig

// Ring buffer persistence for SHIP pairing replay protection (optional, set before Setup)
ringBufferPersistence shipapi.RingBufferPersistence
}

// Setup a Configuration with the required parameters
Expand All @@ -108,7 +114,8 @@ type Configuration struct {
// - port: The port address of the websocket server, required
// - certificate: The certificate used for the service and its connections, required
// - heartbeatTimeout: The timeout to be used for sending heartbeats and applied to all local entities created on setup of the service
// - mdnsProviderSelection: Optional set which mDNS providers should be used, default is `mdns.MdnsProviderSelectionAll`
// - pairingConfig: Optional SHIP Pairing configuration. Pass nil if SHIP Pairing should not be used
// - ringBufferPersistence: Interface to Pairing ring buffer persistence. SHALL be set if pairingConfig is set to listener mode (i.e. devA). See ship-go examples on how to create an implementation of this interface
//
// Returns:
// - *Configuration: The created configuration
Expand All @@ -124,6 +131,8 @@ func NewConfiguration(
port int,
certificate tls.Certificate,
heartbeatTimeout time.Duration,
pairingConfig *shipapi.PairingConfig,
ringBufferPersistence shipapi.RingBufferPersistence,
) (*Configuration, error) {
configuration := &Configuration{
certificate: certificate,
Expand Down Expand Up @@ -176,6 +185,14 @@ func NewConfiguration(
// set default
configuration.featureSet = model.NetworkManagementFeatureSetTypeSmart

configuration.pairingConfig = pairingConfig
if pc := configuration.pairingConfig; pc != nil &&
(pc.Mode == shipapi.PairingModeListener || pc.Mode == shipapi.PairingModeBoth) &&
ringBufferPersistence == nil {
return nil, fmt.Errorf("ringBufferPersistence interface %s for pairing mode listener", isRequired)
}
configuration.ringBufferPersistence = ringBufferPersistence

return configuration, nil
}

Expand Down Expand Up @@ -304,3 +321,11 @@ func (s *Configuration) SetCertificate(cert tls.Certificate) {
func (s *Configuration) HeartbeatTimeout() time.Duration {
return s.heartbeatTimeout
}

func (s *Configuration) PairingConfig() *shipapi.PairingConfig {
return s.pairingConfig
}

func (s *Configuration) RingBufferPersistence() shipapi.RingBufferPersistence {
return s.ringBufferPersistence
}
Loading
Loading