mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-04 18:31:41 +00:00
@@ -0,0 +1,469 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/sonr-io/sonr/types/webauthn/metadata"
|
||||
)
|
||||
|
||||
// TrustAnchor represents a trusted root certificate or public key for attestation validation
|
||||
type TrustAnchor struct {
|
||||
// Format is the attestation format this anchor applies to (e.g., "packed", "tpm", "android-safetynet")
|
||||
Format string
|
||||
// AAGUID is the Authenticator Attestation GUID this anchor applies to (optional)
|
||||
AAGUID []byte
|
||||
// RootCertificate is the trusted root certificate (for X.509 chains)
|
||||
RootCertificate []byte
|
||||
// PublicKey is the trusted public key (for ECDAA or direct attestation)
|
||||
PublicKey []byte
|
||||
// Description provides human-readable information about this trust anchor
|
||||
Description string
|
||||
}
|
||||
|
||||
// CredentialPolicy defines the policy for accepting credentials
|
||||
type CredentialPolicy struct {
|
||||
// AllowSelfAttestation permits self-attestation (not recommended for high-security scenarios)
|
||||
AllowSelfAttestation bool
|
||||
// RequireAttestation requires attestation to be present and valid
|
||||
RequireAttestation bool
|
||||
// TrustAnchors is the list of acceptable trust anchors for attestation validation
|
||||
TrustAnchors []TrustAnchor
|
||||
// AllowedAAGUIDs restricts registration to specific authenticator models (if empty, all are allowed)
|
||||
AllowedAAGUIDs [][]byte
|
||||
// MinimumAuthenticatorLevel specifies the minimum authenticator certification level (1-3, 0 = no requirement)
|
||||
MinimumAuthenticatorLevel int
|
||||
}
|
||||
|
||||
// DefaultPolicy returns a sensible default policy for most applications
|
||||
func DefaultPolicy() *CredentialPolicy {
|
||||
return &CredentialPolicy{
|
||||
AllowSelfAttestation: true, // Allow self-attestation for broad compatibility
|
||||
RequireAttestation: false, // Don't require attestation by default
|
||||
TrustAnchors: []TrustAnchor{},
|
||||
AllowedAAGUIDs: [][]byte{},
|
||||
MinimumAuthenticatorLevel: 0, // No certification requirement by default
|
||||
}
|
||||
}
|
||||
|
||||
// HighSecurityPolicy returns a strict policy for high-security applications
|
||||
func HighSecurityPolicy() *CredentialPolicy {
|
||||
return &CredentialPolicy{
|
||||
AllowSelfAttestation: false, // Reject self-attestation
|
||||
RequireAttestation: true, // Require valid attestation
|
||||
TrustAnchors: []TrustAnchor{}, // Must be populated with specific trust anchors
|
||||
AllowedAAGUIDs: [][]byte{}, // Should be populated with known secure authenticators
|
||||
MinimumAuthenticatorLevel: 2, // Require at least Level 2 certification
|
||||
}
|
||||
}
|
||||
|
||||
// Credential is the basic credential type from the Credential Management specification that is inherited by WebAuthn's
|
||||
// PublicKeyCredential type.
|
||||
//
|
||||
// Specification: Credential Management §2.2. The Credential Interface (https://www.w3.org/TR/credential-management/#credential)
|
||||
type Credential struct {
|
||||
// ID is The credential’s identifier. The requirements for the
|
||||
// identifier are distinct for each type of credential. It might
|
||||
// represent a username for username/password tuples, for example.
|
||||
ID string `json:"id"`
|
||||
// Type is the value of the object’s interface object's [[type]] slot,
|
||||
// which specifies the credential type represented by this object.
|
||||
// This should be type "public-key" for Webauthn credentials.
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ParsedCredential is the parsed PublicKeyCredential interface, inherits from Credential, and contains
|
||||
// the attributes that are returned to the caller when a new credential is created, or a new assertion is requested.
|
||||
type ParsedCredential struct {
|
||||
ID string `cbor:"id"`
|
||||
Type string `cbor:"type"`
|
||||
}
|
||||
|
||||
type PublicKeyCredential struct {
|
||||
Credential
|
||||
|
||||
RawID URLEncodedBase64 `json:"rawId"`
|
||||
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
|
||||
AuthenticatorAttachment string `json:"authenticatorAttachment,omitempty"`
|
||||
}
|
||||
|
||||
type ParsedPublicKeyCredential struct {
|
||||
ParsedCredential
|
||||
|
||||
RawID []byte `json:"rawId"`
|
||||
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
|
||||
AuthenticatorAttachment AuthenticatorAttachment `json:"authenticatorAttachment,omitempty"`
|
||||
}
|
||||
|
||||
type CredentialCreationResponse struct {
|
||||
PublicKeyCredential
|
||||
|
||||
AttestationResponse AuthenticatorAttestationResponse `json:"response"`
|
||||
}
|
||||
|
||||
// Implement WebAuthnCredential interface for CredentialCreationResponse
|
||||
// This allows the protocol's credential types to work with Sonr's centralized validation
|
||||
|
||||
func (ccr *CredentialCreationResponse) GetCredentialId() string {
|
||||
return ccr.ID
|
||||
}
|
||||
|
||||
func (ccr *CredentialCreationResponse) GetPublicKey() []byte {
|
||||
// URLEncodedBase64 is already []byte, so we can return it directly
|
||||
return []byte(ccr.AttestationResponse.PublicKey)
|
||||
}
|
||||
|
||||
func (ccr *CredentialCreationResponse) GetAlgorithm() int32 {
|
||||
return int32(ccr.AttestationResponse.PublicKeyAlgorithm)
|
||||
}
|
||||
|
||||
func (ccr *CredentialCreationResponse) GetRawId() string {
|
||||
return string(ccr.RawID)
|
||||
}
|
||||
|
||||
func (ccr *CredentialCreationResponse) GetClientDataJson() string {
|
||||
return string(ccr.AttestationResponse.ClientDataJSON)
|
||||
}
|
||||
|
||||
func (ccr *CredentialCreationResponse) GetAttestationObject() string {
|
||||
return string(ccr.AttestationResponse.AttestationObject)
|
||||
}
|
||||
|
||||
func (ccr *CredentialCreationResponse) GetOrigin() string {
|
||||
// Parse the origin from ClientDataJSON
|
||||
// The ClientDataJSON contains the origin field when parsed
|
||||
|
||||
// Try to parse the credential to get the collected client data
|
||||
parsed, err := ccr.Parse()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return parsed.Response.CollectedClientData.Origin
|
||||
}
|
||||
|
||||
type ParsedCredentialCreationData struct {
|
||||
ParsedPublicKeyCredential
|
||||
|
||||
Response ParsedAttestationResponse
|
||||
Raw CredentialCreationResponse
|
||||
}
|
||||
|
||||
// Implement WebAuthnCredential interface for ParsedCredentialCreationData
|
||||
// This allows the parsed credential types to work with Sonr's centralized validation
|
||||
|
||||
func (pcc *ParsedCredentialCreationData) GetCredentialId() string {
|
||||
return pcc.ID
|
||||
}
|
||||
|
||||
func (pcc *ParsedCredentialCreationData) GetPublicKey() []byte {
|
||||
// Get the public key from the parsed authenticator data
|
||||
if len(pcc.Response.AttestationObject.AuthData.AttData.CredentialPublicKey) > 0 {
|
||||
return pcc.Response.AttestationObject.AuthData.AttData.CredentialPublicKey
|
||||
}
|
||||
// Fallback to raw credential public key if available
|
||||
return []byte(pcc.Raw.AttestationResponse.PublicKey)
|
||||
}
|
||||
|
||||
func (pcc *ParsedCredentialCreationData) GetAlgorithm() int32 {
|
||||
return int32(pcc.Raw.AttestationResponse.PublicKeyAlgorithm)
|
||||
}
|
||||
|
||||
func (pcc *ParsedCredentialCreationData) GetRawId() string {
|
||||
return string(pcc.RawID)
|
||||
}
|
||||
|
||||
func (pcc *ParsedCredentialCreationData) GetClientDataJson() string {
|
||||
return string(pcc.Raw.AttestationResponse.ClientDataJSON)
|
||||
}
|
||||
|
||||
func (pcc *ParsedCredentialCreationData) GetAttestationObject() string {
|
||||
return string(pcc.Raw.AttestationResponse.AttestationObject)
|
||||
}
|
||||
|
||||
func (pcc *ParsedCredentialCreationData) GetOrigin() string {
|
||||
return pcc.Response.CollectedClientData.Origin
|
||||
}
|
||||
|
||||
// ParseCredentialCreationResponse is a non-agnostic function for parsing a registration response from the http library
|
||||
// from stdlib. It handles some standard cleanup operations.
|
||||
func ParseCredentialCreationResponse(request *http.Request) (*ParsedCredentialCreationData, error) {
|
||||
if request == nil || request.Body == nil {
|
||||
return nil, ErrBadRequest.WithDetails("No response given")
|
||||
}
|
||||
|
||||
defer request.Body.Close()
|
||||
defer io.Copy(io.Discard, request.Body)
|
||||
|
||||
return ParseCredentialCreationResponseBody(request.Body)
|
||||
}
|
||||
|
||||
// ParseCredentialCreationResponseBody is an agnostic version of ParseCredentialCreationResponse. Implementers are
|
||||
// therefore responsible for managing cleanup.
|
||||
func ParseCredentialCreationResponseBody(
|
||||
body io.Reader,
|
||||
) (pcc *ParsedCredentialCreationData, err error) {
|
||||
var ccr CredentialCreationResponse
|
||||
|
||||
if err = decodeBody(body, &ccr); err != nil {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").
|
||||
WithInfo(err.Error()).
|
||||
WithError(err)
|
||||
}
|
||||
|
||||
return ccr.Parse()
|
||||
}
|
||||
|
||||
// ParseCredentialCreationResponseBytes is an alternative version of ParseCredentialCreationResponseBody that just takes
|
||||
// a byte slice.
|
||||
func ParseCredentialCreationResponseBytes(
|
||||
data []byte,
|
||||
) (pcc *ParsedCredentialCreationData, err error) {
|
||||
var ccr CredentialCreationResponse
|
||||
|
||||
if err = decodeBytes(data, &ccr); err != nil {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").
|
||||
WithInfo(err.Error()).
|
||||
WithError(err)
|
||||
}
|
||||
|
||||
return ccr.Parse()
|
||||
}
|
||||
|
||||
// Parse validates and parses the CredentialCreationResponse into a ParsedCredentialCreationData. This receiver
|
||||
// is unlikely to be expressly guaranteed under the versioning policy. Users looking for this guarantee should see
|
||||
// ParseCredentialCreationResponseBody instead, and this receiver should only be used if that function is inadequate
|
||||
// for their use case.
|
||||
func (ccr CredentialCreationResponse) Parse() (pcc *ParsedCredentialCreationData, err error) {
|
||||
if ccr.ID == "" {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing ID")
|
||||
}
|
||||
|
||||
testB64, err := base64.RawURLEncoding.DecodeString(ccr.ID)
|
||||
if err != nil || !(len(testB64) > 0) {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").
|
||||
WithInfo("ID not base64.RawURLEncoded")
|
||||
}
|
||||
|
||||
if ccr.PublicKeyCredential.Credential.Type == "" {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").
|
||||
WithInfo("Missing type")
|
||||
}
|
||||
|
||||
if ccr.PublicKeyCredential.Credential.Type != string(PublicKeyCredentialType) {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").
|
||||
WithInfo("Type not public-key")
|
||||
}
|
||||
|
||||
response, err := ccr.AttestationResponse.Parse()
|
||||
if err != nil {
|
||||
return nil, ErrParsingData.WithDetails("Error parsing attestation response")
|
||||
}
|
||||
|
||||
var attachment AuthenticatorAttachment
|
||||
|
||||
switch ccr.AuthenticatorAttachment {
|
||||
case "platform":
|
||||
attachment = Platform
|
||||
case "cross-platform":
|
||||
attachment = CrossPlatform
|
||||
}
|
||||
|
||||
return &ParsedCredentialCreationData{
|
||||
ParsedPublicKeyCredential{
|
||||
ParsedCredential{ccr.ID, ccr.Type}, ccr.RawID, ccr.ClientExtensionResults, attachment,
|
||||
},
|
||||
*response,
|
||||
ccr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Verify the Client and Attestation data.
|
||||
//
|
||||
// Specification: §7.1. Registering a New Credential (https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential)
|
||||
func (pcc *ParsedCredentialCreationData) Verify(
|
||||
storedChallenge string,
|
||||
verifyUser bool,
|
||||
verifyUserPresence bool,
|
||||
relyingPartyID string,
|
||||
rpOrigins, rpTopOrigins []string,
|
||||
rpTopOriginsVerify TopOriginVerificationMode,
|
||||
mds metadata.Provider,
|
||||
credParams []CredentialParameter,
|
||||
) (clientDataHash []byte, err error) {
|
||||
// Use default policy if none provided
|
||||
return pcc.VerifyWithPolicy(storedChallenge, verifyUser, verifyUserPresence, relyingPartyID,
|
||||
rpOrigins, rpTopOrigins, rpTopOriginsVerify, mds, credParams, DefaultPolicy())
|
||||
}
|
||||
|
||||
// VerifyWithPolicy verifies the Client and Attestation data with a custom credential policy.
|
||||
//
|
||||
// Specification: §7.1. Registering a New Credential (https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential)
|
||||
func (pcc *ParsedCredentialCreationData) VerifyWithPolicy(
|
||||
storedChallenge string,
|
||||
verifyUser bool,
|
||||
verifyUserPresence bool,
|
||||
relyingPartyID string,
|
||||
rpOrigins, rpTopOrigins []string,
|
||||
rpTopOriginsVerify TopOriginVerificationMode,
|
||||
mds metadata.Provider,
|
||||
credParams []CredentialParameter,
|
||||
policy *CredentialPolicy,
|
||||
) (clientDataHash []byte, err error) {
|
||||
// Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data
|
||||
if err = pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Step 7. Compute the hash of response.clientDataJSON using SHA-256.
|
||||
sum := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON)
|
||||
clientDataHash = sum[:]
|
||||
|
||||
// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
|
||||
// structure to obtain the attestation statement format fmt, the authenticator data authData, and the
|
||||
// attestation statement attStmt.
|
||||
|
||||
// We do the above step while parsing and decoding the CredentialCreationResponse
|
||||
// Handle steps 9 through 14 - This verifies the attestation object.
|
||||
if err = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash, verifyUser, verifyUserPresence, mds, credParams); err != nil {
|
||||
return clientDataHash, err
|
||||
}
|
||||
|
||||
// Step 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root
|
||||
// certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement
|
||||
// format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service provides
|
||||
// one way to obtain such information, using the AAGUID in the attestedCredentialData in authData.
|
||||
// [https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-service-v2.0-id-20180227.html]
|
||||
|
||||
// Apply policy validation if provided
|
||||
if policy != nil {
|
||||
// Check if attestation is required
|
||||
if policy.RequireAttestation && pcc.Response.AttestationObject.Format == "none" {
|
||||
return clientDataHash, ErrAttestationFormat.WithDetails(
|
||||
"Attestation required by policy but not provided",
|
||||
)
|
||||
}
|
||||
|
||||
// Check AAGUID restrictions
|
||||
if len(policy.AllowedAAGUIDs) > 0 {
|
||||
aaguid := pcc.Response.AttestationObject.AuthData.AttData.AAGUID
|
||||
allowed := false
|
||||
for _, allowedAAGUID := range policy.AllowedAAGUIDs {
|
||||
if string(aaguid) == string(allowedAAGUID) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return clientDataHash, ErrAttestationFormat.WithDetails(
|
||||
"Authenticator AAGUID not in allowed list",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 16. Assess the attestation trustworthiness using outputs of the verification procedure in step 14, as follows:
|
||||
// - If self attestation was used, check if self attestation is acceptable under Relying Party policy.
|
||||
// - If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in
|
||||
// the set of acceptable trust anchors obtained in step 15.
|
||||
// - Otherwise, use the X.509 certificates returned by the verification procedure to verify that the
|
||||
// attestation public key correctly chains up to an acceptable root certificate.
|
||||
|
||||
// Check self-attestation policy
|
||||
if policy != nil && !policy.AllowSelfAttestation {
|
||||
// Check if this is self-attestation (format is "packed" with no x5c chain)
|
||||
if pcc.Response.AttestationObject.Format == "packed" {
|
||||
// Self-attestation in packed format has no x5c certificate chain
|
||||
if _, hasX5C := pcc.Response.AttestationObject.AttStatement["x5c"]; !hasX5C {
|
||||
return clientDataHash, ErrAttestationFormat.WithDetails(
|
||||
"Self-attestation not allowed by policy",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 17. Check that the credentialId is not yet registered to any other user. If registration is
|
||||
// requested for a credential that is already registered to a different user, the Relying Party SHOULD
|
||||
// fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting
|
||||
// the older registration.
|
||||
|
||||
// Note: The Relying Party must check for duplicate credential IDs against their database.
|
||||
// This cannot be enforced at the library level.
|
||||
|
||||
// Step 18 If the attestation statement attStmt verified successfully and is found to be trustworthy, then
|
||||
// register the new credential with the account that was denoted in the options.user passed to create(), by
|
||||
// associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as
|
||||
// appropriate for the Relying Party's system.
|
||||
|
||||
// Step 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above,
|
||||
// the Relying Party SHOULD fail the registration ceremony.
|
||||
|
||||
// Policy validation has been implemented above to handle trust assessment
|
||||
|
||||
return clientDataHash, nil
|
||||
}
|
||||
|
||||
// GetAppID takes a AuthenticationExtensions object or nil. It then performs the following checks in order:
|
||||
//
|
||||
// 1. Check that the Session Data's AuthenticationExtensions has been provided and if it hasn't return an error.
|
||||
// 2. Check that the AuthenticationExtensionsClientOutputs contains the extensions output and return an empty string if it doesn't.
|
||||
// 3. Check that the Credential AttestationType is `fido-u2f` and return an empty string if it isn't.
|
||||
// 4. Check that the AuthenticationExtensionsClientOutputs contains the appid key and if it doesn't return an empty string.
|
||||
// 5. Check that the AuthenticationExtensionsClientOutputs appid is a bool and if it isn't return an error.
|
||||
// 6. Check that the appid output is true and if it isn't return an empty string.
|
||||
// 7. Check that the Session Data has an appid extension defined and if it doesn't return an error.
|
||||
// 8. Check that the appid extension in Session Data is a string and if it isn't return an error.
|
||||
// 9. Return the appid extension value from the Session data.
|
||||
func (ppkc ParsedPublicKeyCredential) GetAppID(
|
||||
authExt AuthenticationExtensions,
|
||||
credentialAttestationType string,
|
||||
) (appID string, err error) {
|
||||
var (
|
||||
value, clientValue any
|
||||
enableAppID, ok bool
|
||||
)
|
||||
|
||||
if authExt == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if ppkc.ClientExtensionResults == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// If the credential does not have the correct attestation type it is assumed to NOT be a fido-u2f credential.
|
||||
// https://www.w3.org/TR/webauthn/#sctn-fido-u2f-attestation
|
||||
if credentialAttestationType != CredentialTypeFIDOU2F {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if clientValue, ok = ppkc.ClientExtensionResults[ExtensionAppID]; !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if enableAppID, ok = clientValue.(bool); !ok {
|
||||
return "", ErrBadRequest.WithDetails("Client Output appid did not have the expected type")
|
||||
}
|
||||
|
||||
if !enableAppID {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if value, ok = authExt[ExtensionAppID]; !ok {
|
||||
return "", ErrBadRequest.WithDetails(
|
||||
"Session Data does not have an appid but Client Output indicates it should be set",
|
||||
)
|
||||
}
|
||||
|
||||
if appID, ok = value.(string); !ok {
|
||||
return "", ErrBadRequest.WithDetails("Session Data appid did not have the expected type")
|
||||
}
|
||||
|
||||
return appID, nil
|
||||
}
|
||||
|
||||
const (
|
||||
CredentialTypeFIDOU2F = "fido-u2f"
|
||||
)
|
||||
Reference in New Issue
Block a user