Skip to content

SHIP Pairing Implementation and API redesign#66

Open
DerAndereAndi wants to merge 20 commits intodevfrom
feature/shippairing-2
Open

SHIP Pairing Implementation and API redesign#66
DerAndereAndi wants to merge 20 commits intodevfrom
feature/shippairing-2

Conversation

@DerAndereAndi
Copy link
Copy Markdown
Member

Implements SHIP Pairing Service specification including automatic device pairing, HMAC-based authentication, and Device Replacement Timing Logic for AddCu devices.

BREAKING CHANGE: ServiceIdentity is now used instead of ServiceDetails, or manual SKI, fingerprint, shipID arguments

Key components:

  • Complete pairing service with listener and announcer functionality
  • HMAC-SHA256 authentication with nonce-based replay protection
  • Ring buffer digest validation preventing replay attacks
  • QR code generation and parsing for device discovery
  • 15-minute AddCu device replacement timer with automatic trust management
  • ServiceDetails-centric Hub API replacing SKI-string methods
  • Thread-safe AddCuReplacementTracker for production environments
  • Comprehensive test coverage with specification compliance validation

Architecture improvements:

  • Enhanced ServiceDetails with fingerprint, PairingType, and Copy() methods
  • Hub interface modernized to use ServiceDetails objects consistently
  • Multi-identifier service lookup (SKI, fingerprint, SHIP ID)
  • Clean separation between pairing service and Hub functionality
  • Interface-based design enabling flexible pairing strategies

Security features:

  • Constant-time HMAC validation preventing timing attacks
  • Secure nonce generation with cryptographic randomness
  • Certificate fingerprint validation for device identity verification
  • Memory clearing of sensitive pairing data
  • Protection against concurrent access races with Copy() pattern

Specification compliance:

  • Device Replacement Timing Logic per section 4.3
  • Automatic trust establishment after successful HMAC verification
  • Timer-based pairing listener reactivation
  • Backward compatibility with existing SHIP 1.0.1 connections

@DerAndereAndi DerAndereAndi added the enhancement New feature or request label Feb 26, 2026
@DerAndereAndi DerAndereAndi added this to the Version 0.7 milestone Feb 26, 2026
kirollosnct and others added 6 commits February 27, 2026 06:48
Co-Authored-By: Mostafa Abdelfatah <Mostafa.Abdelfattah@coretech-innovations.com>
…plete

Co-Authored-By: Mostafa Abdelfatah <Mostafa.Abdelfattah@coretech-innovations.com>
- enforce devA-side forPar matching and runtime trustCurve support checks
- require devZ target trust before StartAnnouncementTo and validate target secret bound
- harden _shippairing TXT ingestion (enum validation + nonce/digest format checks)
- normalize pairing secret length handling across config/listener/announcer paths
- add and update TDD coverage for listener, hub, mdns and secret boundary cases
- [HubInterface] Add SHIP Pairing Service APIs and other missing APIs that were not covered by the interface.
- [SHIP Pairing Interfaces & Implementation]: Remove dead code and re-evaluate some parameters/fields types.
- [Announcement Lifetime Tracker]: changed timers map to take lifetimeTimer as a value instead of a pointer reference. + Improve StopAll()
- [Hub] GeneratePairingQR(): Remove secret key parameter - it is fetched internally instead
- Update mocks based on updated APIs
1. In multiple instances, a new instance of ServiceIdentity is created
from SKIToServiceIdentity(), which is incorrect because it creates
a ServiceIdentity object with SKI only. The correct way is to get
the ServiceIdentity from the ServiceDetails itself, unless ServiceDetails
does not exist.
2. In ReportServiceShipID(), we do not need to call RemoteServiceConnected again,
as this gets called after the handshake process is completed.
@kirollosnct kirollosnct force-pushed the feature/shippairing-2 branch from 197ec1b to a59d876 Compare March 16, 2026 00:58
@sthelen-enqs sthelen-enqs self-requested a review March 16, 2026 14:35
…on initiation

While iterating through discovered services on mDNS, the SKI would be used
to search the ServiceDetails for a trust, which would trigger a connection
attempt.
In some cases, during a SHIP Pairing process, devZ can add to its trust
store received information from a devA (SHIP ID and Fingerprint).
This commit will look for the SHIP ID if the SKI was not found for this entry,
on a condition that the fingerprint is present.
@kirollosnct kirollosnct force-pushed the feature/shippairing-2 branch 3 times, most recently from 52b5f20 to 27a8423 Compare March 26, 2026 23:37
@kirollosnct kirollosnct force-pushed the feature/shippairing-2 branch from 27a8423 to 2b11574 Compare March 26, 2026 23:42
Copy link
Copy Markdown
Contributor

@sthelen-enqs sthelen-enqs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've checked over the first few files and found a couple of minor nits and some other small changes ad improvements we could make here.

I'll continue looking over the rest of this PR, but wanted to post these comments so they aren't blocked until I finish the whole review.

Comment thread api/hub.go Outdated
// Used by devA only.
//
// Returns: the fingerprint and ShipID of any trusted AddCu device, respectively. Or empty string if none
HasTrustedAddCuDevice() (string, string)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would normally expect a Has.*() function to only return bool, could we change this to GetTrustedAddCuDevice() instead?

I also don't love the idea of returning 2 strings here and expecting the user to know which is the fingerprint and which is the ShipID. Can we return the ServiceDetail here directly instead?
That way a user could call ShipID() or Fingerprint() on it to get the information they want.

Comment thread api/service_identity.go Outdated
// a service was paired using SHIP Pairing.
//
// Note: This needs to be persisted in the applications trust store and provided with
// RegisterRemoveService call if known!
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// RegisterRemoveService call if known!
// RegisterRemoteService call if known!

Comment thread api/service_identity.go Outdated
// It will be provided instead of SKI when a service is paired using SHIP Pairing.
//
// Note: This needs to be persisted in the applications trust store and provided with
// RegisterRemoveService call if known!
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// RegisterRemoveService call if known!
// RegisterRemoteService call if known!

Comment thread api/service_identity.go Outdated
// a service was paired using SHIP Pairing.
//
// Note: This needs to be persisted in the applications trust store and provided with
// RegisterRemoveService call if known!
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// RegisterRemoveService call if known!
// RegisterRemoteService call if known!

Comment thread api/service_identity.go Outdated
// paired using SHIP Pairing this will be set to PairingTypeAddCu (1)
//
// Note: This needs to be persisted in the applications trust store and provided with
// RegisterRemoveService call!
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// RegisterRemoveService call!
// RegisterRemoteService call!

Comment thread api/shippairing.go Outdated
Comment on lines +193 to +195
//
// This interface replaces PairingHistoryProviderInterface to simplify application implementation
// by moving ring buffer algorithm complexity into the library where it belongs.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//
// This interface replaces PairingHistoryProviderInterface to simplify application implementation
// by moving ring buffer algorithm complexity into the library where it belongs.

see below

Comment thread api/shippairing.go Outdated
Comment on lines +247 to +284
// PairingHistoryProviderInterface - DEPRECATED: Use RingBufferPersistence instead
// This interface will be removed in a future version. Applications should migrate to
// RingBufferPersistence which simplifies implementation by handling ring buffer logic
// in the library rather than requiring applications to implement it.
//
// Applications implement this to provide persistent storage for SHIP pairing history
type PairingHistoryProviderInterface interface {
// HasSeenDigest checks if an HMAC digest has been used in a previous pairing attempt.
// This implements replay attack protection as required by SHIP Pairing Service
// specification section 9.2. Applications must maintain a history of seen digests
// to prevent attackers from reusing captured pairing requests.
//
// Parameters:
// - alg: The HMAC algorithm used (typically "hmacSha256")
// - digest: The hex-encoded HMAC digest to check
//
// Returns:
// - true if this digest has been seen before (potential replay attack)
// - false if this is a new, previously unseen digest
//
// Implementation note: Applications should use efficient storage (e.g., hash set)
// and implement ring buffer behavior per SHIP spec section 11
HasSeenDigest(alg, digest string) bool

// RecordPairing stores a successful pairing's HMAC digest for replay protection.
// This implements the digest history requirement from SHIP Pairing Service
// specification section 11. Applications must maintain a ring buffer of recent
// successful pairings to prevent digest reuse.
//
// Parameters:
// - alg: The HMAC algorithm used (typically "hmacSha256")
// - digest: The hex-encoded HMAC digest from the successful pairing
//
// Implementation note: Applications should implement ring buffer behavior
// to limit memory usage while maintaining sufficient history for security
RecordPairing(alg, digest string)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding a new API that's already deprecated? Can't we just delete this so there's only one API? There shouldn't be any consumers of this

Suggested change
// PairingHistoryProviderInterface - DEPRECATED: Use RingBufferPersistence instead
// This interface will be removed in a future version. Applications should migrate to
// RingBufferPersistence which simplifies implementation by handling ring buffer logic
// in the library rather than requiring applications to implement it.
//
// Applications implement this to provide persistent storage for SHIP pairing history
type PairingHistoryProviderInterface interface {
// HasSeenDigest checks if an HMAC digest has been used in a previous pairing attempt.
// This implements replay attack protection as required by SHIP Pairing Service
// specification section 9.2. Applications must maintain a history of seen digests
// to prevent attackers from reusing captured pairing requests.
//
// Parameters:
// - alg: The HMAC algorithm used (typically "hmacSha256")
// - digest: The hex-encoded HMAC digest to check
//
// Returns:
// - true if this digest has been seen before (potential replay attack)
// - false if this is a new, previously unseen digest
//
// Implementation note: Applications should use efficient storage (e.g., hash set)
// and implement ring buffer behavior per SHIP spec section 11
HasSeenDigest(alg, digest string) bool
// RecordPairing stores a successful pairing's HMAC digest for replay protection.
// This implements the digest history requirement from SHIP Pairing Service
// specification section 11. Applications must maintain a ring buffer of recent
// successful pairings to prevent digest reuse.
//
// Parameters:
// - alg: The HMAC algorithm used (typically "hmacSha256")
// - digest: The hex-encoded HMAC digest from the successful pairing
//
// Implementation note: Applications should implement ring buffer behavior
// to limit memory usage while maintaining sufficient history for security
RecordPairing(alg, digest string)
}

Comment thread api/shippairing.go
Comment on lines +391 to +397
// IsValidLength reports whether the secret length is acceptable for SHIP pairing.
// This implementation supports:
// - 16 bytes (raw 128-bit secret)
// - 32 bytes (commonly used textual/encoded representation)
func (s PairingSecret) IsValidLength() bool {
return len(s) == 16 || len(s) == 32
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we allowing PairingSecrets both in hex as well as raw byte form without differentiating between the two? This feels like it can only go wrong. I can't find any code in the HMAC calculation that differentiates between the two forms and it looks like it assumes it will always get the byte form.
If my understanding is correct we should drop this to only check for len(s) == 16

Suggested change
// IsValidLength reports whether the secret length is acceptable for SHIP pairing.
// This implementation supports:
// - 16 bytes (raw 128-bit secret)
// - 32 bytes (commonly used textual/encoded representation)
func (s PairingSecret) IsValidLength() bool {
return len(s) == 16 || len(s) == 32
}
// IsValidLength reports whether the secret length is acceptable for SHIP pairing.
// This implementation supports:
// - 16 bytes (raw 128-bit secret)
func (s PairingSecret) IsValidLength() bool {
return len(s) == 16
}

Comment thread examples/client/main.go Outdated
Comment on lines +169 to +170
// ServiceConnectionStateChanged method removed - this was not part of HubReaderInterface
// Connection state updates are handled through ServicePairingDetailUpdate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ServiceConnectionStateChanged method removed - this was not part of HubReaderInterface
// Connection state updates are handled through ServicePairingDetailUpdate

Comment thread hub/addcu_replacement_tracker.go Outdated
return false
}

return t.pairedDeviceShipID == shipID && t.pairedDeviceShipID != ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant check for empty string. t.pairedDeviceShipID will never be "" here since it must be equal to shipID and shipID was checked to not be ""

Either of t.pairedDeviceShipID != "" or shipId == "" can be removed.

@kirollosnct kirollosnct force-pushed the feature/shippairing-2 branch from 072c7f5 to b8bc14a Compare April 6, 2026 11:22
@kirollosnct kirollosnct force-pushed the feature/shippairing-2 branch 2 times, most recently from dee9ed6 to ad19726 Compare April 6, 2026 13:29
@kirollosnct kirollosnct force-pushed the feature/shippairing-2 branch from ad19726 to 1aef1cf Compare April 6, 2026 13:36
@kirollosnct
Copy link
Copy Markdown
Member

Thank you @sthelen-enqs for your review and recommendations. Can you please check my latest changes? For the PairingHistoryProviderInterface , I agree that it should neither be marked as a deprecated interface nor exposed for application use. I moved the interface into the pairing package because the interface is used by the pairing struct and also needed for mocking for the unit tests. What do you think about that?

CreateListener and CreateAnnouncer signature changed to take no parameter,
the methods internally will fetch a copy of ServiceDetails instance and
pass it along to the listener/announcer instances.
Replace the provider-instance-ID-as-key approach in pairingInstances with a
new announcedPairing struct and announcedPairings map. AnnouncePairingService
now returns a stable logical ID that callers hold permanently; the internal
providerID is updated transparently on interface-change re-announcements
without invalidating caller-held IDs.
Comment thread hub/hub_connections_server.go Dismissed
…pairing

- Hub pairing tests now pass real SKI/fingerprint/shipID to NewServiceDetails
  instead of empty strings, to be able to create a ServiceDetails.
- pairing.NewService nil-checks the result of api.NewServiceDetails before
  storing it, surfacing a clear error instead of a later nil panic
NewServiceDetails previously returned nil on invalid input (empty SKI
and fingerprint), making it easy for callers to silently propagate a
nil *ServiceDetails and panic at the use site.

Change the signature to (*ServiceDetails, error) so callers must handle
the error explicitly. Update all call sites across api/, hub/, pairing/,
and examples/ to unpack the two return values.

Also nil-guard hub/hub_pairing.go so that an invalid-identity path logs
and returns cleanly instead of panicking.
Replace `defer t.mutex.Unlock()` with an explicit unlock before calling
the timeout callback. sync.RWMutex is not reentrant — holding the write
lock while onTimeout re-enters the tracker (e.g. IsInReplacementWindow)
caused a self-deadlock in the timer goroutine.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants