Files
common/webauthn/credential.go

470 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package webauthn
import (
"crypto/sha256"
"encoding/base64"
"io"
"net/http"
"github.com/sonr-io/common/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 credentials 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 objects 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"
)